Compare commits
4 Commits
8b92867dd2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f645f884d | |||
| 23775e3c67 | |||
| 9b31be5457 | |||
| 1dbf7e33f9 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
/target
|
/target
|
||||||
IMPLEMENTATION_PLAN.md
|
IMPLEMENTATION_PLAN.md
|
||||||
|
|
||||||
# Config files (may contain GitHub tokens)
|
# Config files that may contain GitHub tokens
|
||||||
*.toml
|
# Note: User config files are stored in platform-specific directories outside the repo
|
||||||
config.toml
|
# (e.g., ~/.config/gh-celebs/config.toml on Linux)
|
||||||
|
# This prevents accidental commits if someone creates config.toml in the project root
|
||||||
|
/config.toml
|
||||||
|
|||||||
19
src/cli.rs
19
src/cli.rs
@@ -1,13 +1,10 @@
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::Parser;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "gh-celebs")]
|
#[command(name = "gh-celebs")]
|
||||||
#[command(about = "A fast CLI tool for searching GitHub repositories by popularity")]
|
#[command(about = "A fast CLI tool for searching GitHub repositories by popularity")]
|
||||||
#[command(version)]
|
#[command(version)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[command(subcommand)]
|
|
||||||
pub command: Option<Commands>,
|
|
||||||
|
|
||||||
#[arg(help = "Search query", value_name = "QUERY")]
|
#[arg(help = "Search query", value_name = "QUERY")]
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
|
|
||||||
@@ -16,15 +13,13 @@ pub struct Cli {
|
|||||||
|
|
||||||
#[arg(long, help = "Output results as JSON")]
|
#[arg(long, help = "Output results as JSON")]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[arg(long, help = "Update cached repositories")]
|
||||||
pub enum Commands {
|
pub update: bool,
|
||||||
#[command(about = "Update cached repositories")]
|
|
||||||
Update {
|
|
||||||
#[arg(
|
#[arg(
|
||||||
help = "Repository to update (format: owner/repo). If not specified, updates all cached repos."
|
long,
|
||||||
|
help = "Repository to update (format: owner/repo). Requires --update"
|
||||||
)]
|
)]
|
||||||
repo: Option<String>,
|
pub repo: Option<String>,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,22 @@ pub struct GitHubClient {
|
|||||||
|
|
||||||
impl GitHubClient {
|
impl GitHubClient {
|
||||||
pub fn new(token: Option<String>) -> Result<Self> {
|
pub fn new(token: Option<String>) -> Result<Self> {
|
||||||
|
// Validate token format if provided
|
||||||
|
if let Some(ref t) = token {
|
||||||
|
if !t.is_empty() {
|
||||||
|
// GitHub classic tokens start with 'ghp_'
|
||||||
|
// Fine-grained tokens start with 'github_pat_'
|
||||||
|
// OAuth tokens don't have a specific prefix
|
||||||
|
let is_valid_format = t.starts_with("ghp_") ||
|
||||||
|
t.starts_with("github_pat_") ||
|
||||||
|
t.len() >= 20; // Generic check for reasonable token length
|
||||||
|
|
||||||
|
if !is_valid_format {
|
||||||
|
anyhow::bail!("Invalid GitHub token format. Token should start with 'ghp_' (classic) or 'github_pat_' (fine-grained)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(USER_AGENT, HeaderValue::from_static("gh-celebs"));
|
headers.insert(USER_AGENT, HeaderValue::from_static("gh-celebs"));
|
||||||
headers.insert(ACCEPT, HeaderValue::from_static("application/vnd.github.v3+json"));
|
headers.insert(ACCEPT, HeaderValue::from_static("application/vnd.github.v3+json"));
|
||||||
|
|||||||
13
src/main.rs
13
src/main.rs
@@ -11,7 +11,7 @@ use clap::Parser;
|
|||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::cli::{Cli, Commands};
|
use crate::cli::Cli;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::db::Database;
|
use crate::db::Database;
|
||||||
use crate::github::GitHubClient;
|
use crate::github::GitHubClient;
|
||||||
@@ -51,9 +51,8 @@ async fn main() -> Result<()> {
|
|||||||
let github = GitHubClient::new(config.github.token)?;
|
let github = GitHubClient::new(config.github.token)?;
|
||||||
let search_engine = SearchEngine::new(db, github);
|
let search_engine = SearchEngine::new(db, github);
|
||||||
|
|
||||||
match cli.command {
|
if cli.update {
|
||||||
Some(Commands::Update { repo }) => {
|
if let Some(repo_name) = cli.repo {
|
||||||
if let Some(repo_name) = repo {
|
|
||||||
let rate_limit = search_engine.update_single(&repo_name).await?;
|
let rate_limit = search_engine.update_single(&repo_name).await?;
|
||||||
if rate_limit.remaining < 3 {
|
if rate_limit.remaining < 3 {
|
||||||
eprintln!("Warning: Rate limit running low ({} remaining)", rate_limit.remaining);
|
eprintln!("Warning: Rate limit running low ({} remaining)", rate_limit.remaining);
|
||||||
@@ -64,10 +63,9 @@ async fn main() -> Result<()> {
|
|||||||
eprintln!("Warning: Rate limit running low ({} remaining)", rate_limit.remaining);
|
eprintln!("Warning: Rate limit running low ({} remaining)", rate_limit.remaining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
None => {
|
|
||||||
let query = cli.query.ok_or_else(|| {
|
let query = cli.query.ok_or_else(|| {
|
||||||
anyhow::anyhow!("Query is required when not using a subcommand")
|
anyhow::anyhow!("Query is required. Use --update to update cached repositories.")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let response = search_engine.search(&query, cli.limit).await?;
|
let response = search_engine.search(&query, cli.limit).await?;
|
||||||
@@ -84,7 +82,6 @@ async fn main() -> Result<()> {
|
|||||||
eprintln!("Warning: GitHub API rate limit running low ({} remaining)", response.api_remaining);
|
eprintln!("Warning: GitHub API rate limit running low ({} remaining)", response.api_remaining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ use crate::github::GitHubClient;
|
|||||||
use crate::models::{RateLimitInfo, Repo, SearchResponse, SearchResult};
|
use crate::models::{RateLimitInfo, Repo, SearchResponse, SearchResult};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
pub struct SearchEngine {
|
pub struct SearchEngine {
|
||||||
db: Database,
|
db: Database,
|
||||||
@@ -105,6 +107,12 @@ impl SearchEngine {
|
|||||||
if last_rate_limit.remaining < 3 {
|
if last_rate_limit.remaining < 3 {
|
||||||
println!("\nWarning: Rate limit running low ({} remaining)", last_rate_limit.remaining);
|
println!("\nWarning: Rate limit running low ({} remaining)", last_rate_limit.remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client-side rate limiting: wait 3 seconds between requests
|
||||||
|
// This respects both anonymous (10/min = 6s) and authenticated (30/min = 2s) limits
|
||||||
|
if idx < total - 1 {
|
||||||
|
sleep(Duration::from_secs(3)).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("\n✓ Updated {} repositories", total);
|
println!("\n✓ Updated {} repositories", total);
|
||||||
|
|||||||
Reference in New Issue
Block a user