diff --git a/crates/github/src/auth.rs b/crates/github/src/auth.rs index d4498d0..7309ff2 100644 --- a/crates/github/src/auth.rs +++ b/crates/github/src/auth.rs @@ -6,7 +6,7 @@ impl util::Cmd for Auth { } fn exec(_: &clap::ArgMatches) -> eyre::Result<()> { - util::shell::run(&["gh", "auth", "login"])?; + util::shell::run(&["gh", "auth", "login"], None)?; Ok(()) } diff --git a/crates/github/src/fuzzy_clone.rs b/crates/github/src/fuzzy_clone.rs new file mode 100644 index 0000000..04f4ffd --- /dev/null +++ b/crates/github/src/fuzzy_clone.rs @@ -0,0 +1,258 @@ +use std::io::Write; + +use eyre::Context; + +pub struct FuzzyClone; + +#[derive(Debug)] +struct GitHubEntry { + org: String, + repo: String, +} + +impl GitHubEntry { + fn from(raw: String) -> Option { + let (org, repo) = raw.split_once("/")?; + + Some(GitHubEntry { + org: org.trim().to_string(), + repo: repo.trim().to_string(), + }) + } +} + +struct Settings { + orgs: Vec, + git_root: String, + cache_dir: String, + cache_file_path: String, +} + +impl Settings { + fn new() -> eyre::Result { + let mut cache_dir = + dirs::cache_dir().ok_or(eyre::anyhow!("could not find a valid cache dir"))?; + cache_dir.push("kah-toolkit/github/fc"); + + let mut file_path = std::path::Path::new(&cache_dir).to_path_buf(); + file_path.push("entries"); + + Ok(Self { + orgs: std::env::var("GITHUB_FC_ORGS") + .context("GITHUB_FC_ORGS is not set")? + .split(",") + .map(|s| s.to_string()) + .collect(), + git_root: std::env::var("GITHUB_FC_ROOT").context("GITHUB_FC_ROOT is not set")?, + cache_dir: cache_dir.to_string_lossy().to_string(), + cache_file_path: file_path.to_string_lossy().to_string(), + }) + } +} + +impl FuzzyClone { + fn get_settings() -> eyre::Result { + Settings::new() + } + + fn get_cache() -> eyre::Result> { + let settings = Self::get_settings()?; + let entries = std::fs::read_to_string(settings.cache_file_path)?; + + Self::parse_entries(entries) + } + + fn parse_entries(raw: String) -> eyre::Result> { + let entries = raw + .replace("\r\n", "\n") + .split("\n") + .map(|s| { + s.split_once("/").map(|(org, repo)| { + if let Some((repo, _)) = repo.split_once("\t") { + GitHubEntry { + org: org.to_string(), + repo: repo.to_string(), + } + } else { + GitHubEntry { + org: org.to_string(), + repo: repo.to_string(), + } + } + }) + }) + .filter(|i| i.is_some()) + .map(|i| i.unwrap()) + .collect::>(); + + Ok(entries) + } + + fn cache_exists() -> eyre::Result { + let settings = Self::get_settings()?; + let cf_path = std::path::Path::new(&settings.cache_file_path); + if !cf_path.exists() { + return Ok(false); + } + let metadata_time = cf_path.metadata()?.modified()?; + let cur_time = std::time::SystemTime::now() - std::time::Duration::new(60 * 60 * 24, 0); + + println!( + "metadata_time: {:?}, cur_time: {:?}", + metadata_time, cur_time + ); + + if metadata_time < cur_time { + return Ok(false); + } + + Ok(true) + } + + fn set_cache(input: &Vec) -> eyre::Result<()> { + let settings = Self::get_settings()?; + let cache_file_path = std::path::Path::new(&settings.cache_file_path); + let mut fs = if cache_file_path.exists() { + std::fs::File::create(cache_file_path)? + } else { + let cache_dir_path = std::path::Path::new(&settings.cache_dir); + if !cache_dir_path.exists() { + std::fs::create_dir_all(cache_dir_path)?; + } + std::fs::File::create(cache_file_path)? + }; + + fs.write_all( + input + .iter() + .map(|ge| format!("{}/{}", ge.org, ge.repo)) + .collect::>() + .join("\n") + .as_bytes(), + )?; + + Ok(()) + } + + fn get_entries() -> eyre::Result> { + let mut entries = Vec::new(); + + for org in Self::get_settings()?.orgs { + let private_entires = util::shell::run_with_input_and_output( + &[ + "gh", + "repo", + "list", + &org, + "--visibility", + "private", + "--limit", + "1000", + ], + "".into(), + )?; + + let public_entires = util::shell::run_with_input_and_output( + &[ + "gh", + "repo", + "list", + &org, + "--visibility", + "public", + "--limit", + "1000", + ], + "".into(), + )?; + + let private = std::str::from_utf8(private_entires.stdout.as_slice())?; + let public = std::str::from_utf8(public_entires.stdout.as_slice())?; + let raw_entries = format!("{private}{public}"); + + entries.push(Self::parse_entries(raw_entries)?); + } + + Ok(entries.into_iter().flat_map(|s| s).collect()) + } + + fn clone(chosen: GitHubEntry) -> eyre::Result { + let mut git_path = std::path::Path::new(&Self::get_settings()?.git_root).to_path_buf(); + git_path.push(&chosen.org); + if !git_path.exists() { + std::fs::create_dir_all(&git_path)?; + } + let mut git_repo_path = git_path.clone(); + git_repo_path.push(&chosen.repo); + if !git_repo_path.exists() { + util::shell::run( + &[ + "gh", + "repo", + "clone", + format!("{}/{}", &chosen.org, &chosen.repo).as_str(), + git_repo_path + .to_str() + .ok_or(eyre::anyhow!("could you not transform to path"))?, + ], + Some(util::shell::RunOptions { + path: git_path.clone(), + }), + )?; + } else { + util::shell::run(&["git", "pull"], None)?; + } + + Ok(git_repo_path) + } + + fn run() -> eyre::Result<()> { + let entries = if !Self::cache_exists()? { + let entries = Self::get_entries()?; + Self::set_cache(&entries)?; + entries + } else { + Self::get_cache()? + }; + + let entries_str = entries + .iter() + .map(|ge| format!("{}/{}", ge.org, ge.repo)) + .collect::>() + .join("\n"); + + let chosen = util::shell::run_with_input_and_output(&["fzf"], entries_str)?; + let chosen = std::str::from_utf8(&chosen.stdout)?; + + Self::clone(GitHubEntry::from(chosen.to_string()).ok_or(eyre::anyhow!( + "could not parse choice as github entry /" + ))?)?; + + Ok(()) + } + + fn update() -> eyre::Result<()> { + let entries = Self::get_entries()?; + Self::set_cache(&entries)?; + + Ok(()) + } +} + +impl util::Cmd for FuzzyClone { + fn cmd() -> eyre::Result { + Ok(clap::Command::new("fuzzy-clone") + .alias("fc") + .alias("c") + .subcommand(clap::Command::new("update"))) + } + + fn exec(args: &clap::ArgMatches) -> eyre::Result<()> { + match args.subcommand() { + Some(("update", _)) => Self::update()?, + _ => Self::run()?, + } + + Ok(()) + } +} diff --git a/crates/github/src/gh.rs b/crates/github/src/gh.rs index a33551e..a8bf0c0 100644 --- a/crates/github/src/gh.rs +++ b/crates/github/src/gh.rs @@ -14,7 +14,7 @@ impl Gh { let mut cmd_args = vec!["gh", external]; cmd_args.append(&mut raw.iter().map(|s| &**s).collect()); - util::shell::run(cmd_args.as_slice())?; + util::shell::run(cmd_args.as_slice(), None)?; Ok(()) } @@ -29,7 +29,7 @@ impl util::Cmd for Gh { match args.subcommand() { Some((external, args)) => Self::run(external, args), _ => { - util::shell::run(&["gh"])?; + util::shell::run(&["gh"], None)?; Err(eyre::anyhow!("missing argument")) } diff --git a/crates/github/src/lib.rs b/crates/github/src/lib.rs index 03d6fa9..3071590 100644 --- a/crates/github/src/lib.rs +++ b/crates/github/src/lib.rs @@ -1,4 +1,5 @@ mod auth; +mod fuzzy_clone; mod gh; pub struct GitHub; @@ -17,7 +18,7 @@ impl GitHub { let mut cmd_args = vec!["gh", external]; cmd_args.append(&mut raw.iter().map(|s| &**s).collect()); - util::shell::run(cmd_args.as_slice())?; + util::shell::run(cmd_args.as_slice(), None)?; Ok(()) } @@ -26,13 +27,19 @@ impl GitHub { impl util::Cmd for GitHub { fn cmd() -> eyre::Result { Ok(clap::Command::new("github") - .subcommands(&[auth::Auth::cmd()?, gh::Gh::cmd()?]) + .subcommands(&[ + auth::Auth::cmd()?, + gh::Gh::cmd()?, + fuzzy_clone::FuzzyClone::cmd()?, + ]) .allow_external_subcommands(true)) } fn exec(args: &clap::ArgMatches) -> eyre::Result<()> { match args.subcommand() { Some(("auth", subm)) => auth::Auth::exec(subm), + Some(("fuzzy-clone", subm)) => fuzzy_clone::FuzzyClone::exec(subm), + Some(("fc", subm)) => fuzzy_clone::FuzzyClone::exec(subm), Some(("gh", subm)) => gh::Gh::exec(subm), Some((external, args)) => Self::run(external, args), _ => Err(eyre::anyhow!("missing argument")), diff --git a/crates/sourcegraph/src/auth.rs b/crates/sourcegraph/src/auth.rs index 63d9e3a..2871e98 100644 --- a/crates/sourcegraph/src/auth.rs +++ b/crates/sourcegraph/src/auth.rs @@ -6,7 +6,7 @@ impl util::Cmd for Auth { } fn exec(_: &clap::ArgMatches) -> eyre::Result<()> { - util::shell::run(&["src", "login"])?; + util::shell::run(&["src", "login"], None)?; Ok(()) } diff --git a/crates/sourcegraph/src/lib.rs b/crates/sourcegraph/src/lib.rs index 05f691e..18f7d9c 100644 --- a/crates/sourcegraph/src/lib.rs +++ b/crates/sourcegraph/src/lib.rs @@ -5,7 +5,7 @@ pub struct Sourcegraph; impl Sourcegraph { fn run() -> eyre::Result<()> { - util::shell::run(&["src"])?; + util::shell::run(&["src"], None)?; Err(eyre::anyhow!("missing argument")) } diff --git a/crates/sourcegraph/src/search.rs b/crates/sourcegraph/src/search.rs index 5e276ac..75d387b 100644 --- a/crates/sourcegraph/src/search.rs +++ b/crates/sourcegraph/src/search.rs @@ -24,7 +24,7 @@ impl util::Cmd for Search { let mut cmd_args = vec!["src", "search", external]; cmd_args.append(&mut raw.iter().map(|s| &**s).collect()); - util::shell::run(cmd_args.as_slice())?; + util::shell::run(cmd_args.as_slice(), None)?; } _ => todo!(), } diff --git a/crates/tldr/src/update.rs b/crates/tldr/src/update.rs index b8e1cb5..51fa1c2 100644 --- a/crates/tldr/src/update.rs +++ b/crates/tldr/src/update.rs @@ -25,6 +25,7 @@ impl util::Cmd for Update { .split(" ") .collect::>() .as_slice(), + None, )?; Ok(()) diff --git a/crates/util/src/shell.rs b/crates/util/src/shell.rs index 3e9a021..4e68c9a 100644 --- a/crates/util/src/shell.rs +++ b/crates/util/src/shell.rs @@ -1,21 +1,31 @@ use std::io::Write; -pub fn run(args: &[&str]) -> eyre::Result<()> { - let output = std::process::Command::new( +pub struct RunOptions { + pub path: std::path::PathBuf, +} + +pub fn run(args: &[&str], opts: Option) -> eyre::Result<()> { + let mut cmd = std::process::Command::new( args.first() .ok_or(eyre::anyhow!("could not find first arg"))?, - ) - .args( - args.to_vec() - .into_iter() - .skip(1) - .collect::>() - .as_slice(), - ) - .stdout(std::process::Stdio::inherit()) - .stderr(std::process::Stdio::inherit()) - .stdin(std::process::Stdio::inherit()) - .output(); + ); + + if let Some(opts) = opts { + cmd.current_dir(opts.path); + } + + let output = cmd + .args( + args.to_vec() + .into_iter() + .skip(1) + .collect::>() + .as_slice(), + ) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .stdin(std::process::Stdio::inherit()) + .output(); match output { Ok(o) => {