with fuzzy-clone

This commit is contained in:
Kasper Juul Hermansen 2022-12-18 01:41:12 +01:00
parent 66105004e0
commit 27f63cc1f9
Signed by: kjuulh
GPG Key ID: 0F95C140730F2F23
9 changed files with 298 additions and 22 deletions

View File

@ -6,7 +6,7 @@ impl util::Cmd for Auth {
} }
fn exec(_: &clap::ArgMatches) -> eyre::Result<()> { fn exec(_: &clap::ArgMatches) -> eyre::Result<()> {
util::shell::run(&["gh", "auth", "login"])?; util::shell::run(&["gh", "auth", "login"], None)?;
Ok(()) Ok(())
} }

View File

@ -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<Self> {
let (org, repo) = raw.split_once("/")?;
Some(GitHubEntry {
org: org.trim().to_string(),
repo: repo.trim().to_string(),
})
}
}
struct Settings {
orgs: Vec<String>,
git_root: String,
cache_dir: String,
cache_file_path: String,
}
impl Settings {
fn new() -> eyre::Result<Self> {
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> {
Settings::new()
}
fn get_cache() -> eyre::Result<Vec<GitHubEntry>> {
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<Vec<GitHubEntry>> {
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::<Vec<GitHubEntry>>();
Ok(entries)
}
fn cache_exists() -> eyre::Result<bool> {
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<GitHubEntry>) -> 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::<Vec<String>>()
.join("\n")
.as_bytes(),
)?;
Ok(())
}
fn get_entries() -> eyre::Result<Vec<GitHubEntry>> {
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<std::path::PathBuf> {
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::<Vec<String>>()
.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 <org>/<repo>"
))?)?;
Ok(())
}
fn update() -> eyre::Result<()> {
let entries = Self::get_entries()?;
Self::set_cache(&entries)?;
Ok(())
}
}
impl util::Cmd for FuzzyClone {
fn cmd() -> eyre::Result<clap::Command> {
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(())
}
}

View File

@ -14,7 +14,7 @@ impl Gh {
let mut cmd_args = vec!["gh", external]; let mut cmd_args = vec!["gh", external];
cmd_args.append(&mut raw.iter().map(|s| &**s).collect()); 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(()) Ok(())
} }
@ -29,7 +29,7 @@ impl util::Cmd for Gh {
match args.subcommand() { match args.subcommand() {
Some((external, args)) => Self::run(external, args), Some((external, args)) => Self::run(external, args),
_ => { _ => {
util::shell::run(&["gh"])?; util::shell::run(&["gh"], None)?;
Err(eyre::anyhow!("missing argument")) Err(eyre::anyhow!("missing argument"))
} }

View File

@ -1,4 +1,5 @@
mod auth; mod auth;
mod fuzzy_clone;
mod gh; mod gh;
pub struct GitHub; pub struct GitHub;
@ -17,7 +18,7 @@ impl GitHub {
let mut cmd_args = vec!["gh", external]; let mut cmd_args = vec!["gh", external];
cmd_args.append(&mut raw.iter().map(|s| &**s).collect()); 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(()) Ok(())
} }
@ -26,13 +27,19 @@ impl GitHub {
impl util::Cmd for GitHub { impl util::Cmd for GitHub {
fn cmd() -> eyre::Result<clap::Command> { fn cmd() -> eyre::Result<clap::Command> {
Ok(clap::Command::new("github") 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)) .allow_external_subcommands(true))
} }
fn exec(args: &clap::ArgMatches) -> eyre::Result<()> { fn exec(args: &clap::ArgMatches) -> eyre::Result<()> {
match args.subcommand() { match args.subcommand() {
Some(("auth", subm)) => auth::Auth::exec(subm), 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(("gh", subm)) => gh::Gh::exec(subm),
Some((external, args)) => Self::run(external, args), Some((external, args)) => Self::run(external, args),
_ => Err(eyre::anyhow!("missing argument")), _ => Err(eyre::anyhow!("missing argument")),

View File

@ -6,7 +6,7 @@ impl util::Cmd for Auth {
} }
fn exec(_: &clap::ArgMatches) -> eyre::Result<()> { fn exec(_: &clap::ArgMatches) -> eyre::Result<()> {
util::shell::run(&["src", "login"])?; util::shell::run(&["src", "login"], None)?;
Ok(()) Ok(())
} }

View File

@ -5,7 +5,7 @@ pub struct Sourcegraph;
impl Sourcegraph { impl Sourcegraph {
fn run() -> eyre::Result<()> { fn run() -> eyre::Result<()> {
util::shell::run(&["src"])?; util::shell::run(&["src"], None)?;
Err(eyre::anyhow!("missing argument")) Err(eyre::anyhow!("missing argument"))
} }

View File

@ -24,7 +24,7 @@ impl util::Cmd for Search {
let mut cmd_args = vec!["src", "search", external]; let mut cmd_args = vec!["src", "search", external];
cmd_args.append(&mut raw.iter().map(|s| &**s).collect()); 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!(), _ => todo!(),
} }

View File

@ -25,6 +25,7 @@ impl util::Cmd for Update {
.split(" ") .split(" ")
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.as_slice(), .as_slice(),
None,
)?; )?;
Ok(()) Ok(())

View File

@ -1,10 +1,20 @@
use std::io::Write; use std::io::Write;
pub fn run(args: &[&str]) -> eyre::Result<()> { pub struct RunOptions {
let output = std::process::Command::new( pub path: std::path::PathBuf,
}
pub fn run(args: &[&str], opts: Option<RunOptions>) -> eyre::Result<()> {
let mut cmd = std::process::Command::new(
args.first() args.first()
.ok_or(eyre::anyhow!("could not find first arg"))?, .ok_or(eyre::anyhow!("could not find first arg"))?,
) );
if let Some(opts) = opts {
cmd.current_dir(opts.path);
}
let output = cmd
.args( .args(
args.to_vec() args.to_vec()
.into_iter() .into_iter()