Compare commits

..

1 Commits

5 changed files with 46 additions and 64 deletions

8
.gitignore vendored
View File

@@ -1,8 +1,6 @@
/target /target
IMPLEMENTATION_PLAN.md IMPLEMENTATION_PLAN.md
# Config files that may contain GitHub tokens # Config files (may contain GitHub tokens)
# Note: User config files are stored in platform-specific directories outside the repo *.toml
# (e.g., ~/.config/gh-celebs/config.toml on Linux) config.toml
# This prevents accidental commits if someone creates config.toml in the project root
/config.toml

View File

@@ -1,10 +1,13 @@
use clap::Parser; use clap::{Parser, Subcommand};
#[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>,
@@ -13,13 +16,15 @@ pub struct Cli {
#[arg(long, help = "Output results as JSON")] #[arg(long, help = "Output results as JSON")]
pub json: bool, pub json: bool,
}
#[arg(long, help = "Update cached repositories")]
pub update: bool, #[derive(Subcommand)]
pub enum Commands {
#[arg( #[command(about = "Update cached repositories")]
long, Update {
help = "Repository to update (format: owner/repo). Requires --update" #[arg(
)] help = "Repository to update (format: owner/repo). If not specified, updates all cached repos."
pub repo: Option<String>, )]
repo: Option<String>,
},
} }

View File

@@ -11,22 +11,6 @@ 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"));

View File

@@ -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; use crate::cli::{Cli, Commands};
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,8 +51,9 @@ 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);
if cli.update { match cli.command {
if let Some(repo_name) = cli.repo { Some(Commands::Update { 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);
@@ -63,9 +64,10 @@ 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. Use --update to update cached repositories.") anyhow::anyhow!("Query is required when not using a subcommand")
})?; })?;
let response = search_engine.search(&query, cli.limit).await?; let response = search_engine.search(&query, cli.limit).await?;
@@ -82,6 +84,7 @@ 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(())
} }

View File

@@ -3,8 +3,6 @@ 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,
@@ -107,12 +105,6 @@ 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);