commit 5802b1104cde24c2bf66a523a0ecbaef1e8a8bb5 Author: JMARyA Date: Sat Mar 9 23:40:36 2024 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ed79bb3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,276 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "giterator" +version = "0.1.0" +dependencies = [ + "clap", + "csv", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + +[[package]] +name = "syn" +version = "2.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2adb6e1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "giterator" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.5.2", features = ["cargo"] } +csv = "1.3.0" +serde_json = "1.0.114" diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..f4bae4c --- /dev/null +++ b/src/args.rs @@ -0,0 +1,46 @@ +use clap::{arg, command, ArgMatches}; + +pub fn get_args() -> ArgMatches { + command!() + .about("Iterate over Git commits and run commands") + .arg(arg!([command] "Command to run for each commit").required(true)) + .arg( + arg!([repository] "Git Repository") + .required(false) + .default_value("."), + ) + .arg( + clap::Arg::new("allow-dirty") + .long("allow-dirty") + .help("Allow working with unclean repository") + .num_args(0) + .required(false), + ) + .arg( + clap::Arg::new("script_file") + .short('s') + .long("script") + .help("Use the content of a script file as command") + .num_args(0) + .required(false), + ) + .arg( + clap::Arg::new("json") + .short('j') + .long("json") + .conflicts_with("csv") + .help("Output as JSON") + .num_args(0) + .required(false), + ) + .arg( + clap::Arg::new("csv") + .short('c') + .long("csv") + .conflicts_with("json") + .help("Output as CSV") + .num_args(0) + .required(false), + ) + .get_matches() +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..58ed71c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,207 @@ +use std::process::{Command, Output}; +mod args; + +#[derive(Debug, Clone)] +struct Commit { + repo: String, + hash: String, + datetime: String, + name: String, +} + +struct IterationOutput { + commit: Commit, + stdout: String, + stderr: String, +} + +impl IterationOutput { + pub fn new(commit: Commit, out: Output) -> Self { + Self { + commit, + stdout: String::from_utf8(out.stdout).unwrap(), + stderr: String::from_utf8(out.stderr).unwrap(), + } + } + + fn print_text(&self) { + println!( + "Commit [{}] ({}): {}", + self.commit.hash, self.commit.datetime, self.commit.name + ); + println!("{}", self.stdout); + if !self.stderr.is_empty() { + println!("{}", self.stderr); + } + } + + fn as_json(&self) -> serde_json::Value { + serde_json::json!({ + "repo": std::fs::canonicalize(self.commit.repo.clone()).unwrap().to_str().unwrap().to_string(), + "hash": self.commit.hash, + "datetime": self.commit.datetime, + "name": self.commit.datetime, + "stdout": self.stdout, + "stderr": self.stderr + }) + } + + fn as_csv(&self) -> Vec { + vec![ + self.commit.hash.clone(), + self.commit.datetime.clone(), + self.commit.name.clone(), + std::fs::canonicalize(self.commit.repo.clone()) + .unwrap() + .to_str() + .unwrap() + .to_string(), + self.stdout.replace('\n', "\\n"), + self.stderr.replace('\n', "\\n"), + ] + } +} + +impl Commit { + pub fn run_command(&self, command: &str) -> IterationOutput { + checkout(&self.repo, &self.hash).unwrap(); + // todo : expose env vars + let out = Command::new("sh") + .current_dir(&self.repo) + .arg("-c") + .arg(command) + .output() + .unwrap(); + + IterationOutput::new(self.clone(), out) + } +} + +fn checkout(repo: &str, commit: &str) -> Result<(), std::io::Error> { + let output = Command::new("git") + .current_dir(repo) + .arg("checkout") + .arg(commit) + .output()?; + + if output.status.success() { + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to execute git command", + )) + } +} + +fn get_commit_list(repo: &str) -> Result, std::io::Error> { + let output = Command::new("git") + .current_dir(repo) + .arg("log") + .arg("--pretty=format:%h - %ad - %s") + .arg("--date=iso") + .output()?; + + if output.status.success() { + let mut commits = Vec::new(); + let out = String::from_utf8(output.stdout).unwrap(); + for line in out.lines() { + let mut split = line.split(" - "); + commits.push(Commit { + repo: repo.to_string(), + hash: split.next().unwrap().to_string(), + datetime: split.next().unwrap().to_string(), + name: split.next().unwrap().to_string(), + }); + } + Ok(commits) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to execute git command", + )) + } +} + +fn is_repository_clean(repo: &str) -> bool { + let output = Command::new("git") + .current_dir(repo) + .arg("status") + .arg("--porcelain") + .output() + .unwrap(); + + String::from_utf8(output.stdout).unwrap().is_empty() +} + +enum OutMode { + Text, + Json, + Csv, +} + +impl OutMode { + pub const fn new(json: bool, csv: bool) -> Self { + if json { + Self::Json + } else if csv { + Self::Csv + } else { + Self::Text + } + } +} + +fn main() { + let args = args::get_args(); + + let repo = args.get_one::("repository").unwrap(); + let allow_dirty = args.get_flag("allow-dirty"); + let command = if args.get_flag("script_file") { + std::fs::read_to_string(args.get_one::("command").unwrap()).unwrap() + } else { + args.get_one::("command").unwrap().clone() + }; + let outmode = OutMode::new(args.get_flag("json"), args.get_flag("csv")); + + let mut out = Vec::new(); + + if is_repository_clean(repo) || allow_dirty { + let commits = get_commit_list(repo).unwrap(); + for commit in commits { + // todo : add colors + out.push(commit.run_command(&command)); + } + checkout(repo, "main").unwrap(); + + match outmode { + OutMode::Text => { + for i in out { + i.print_text(); + } + } + OutMode::Json => { + let json: Vec<_> = out.into_iter().map(|x| x.as_json()).collect(); + println!( + "{}", + serde_json::to_string(&serde_json::to_value(json).unwrap()).unwrap() + ); + } + OutMode::Csv => { + let csv: Vec> = out.into_iter().map(|x| x.as_csv()).collect(); + let mut wtr = csv::Writer::from_writer(std::io::stdout()); + + wtr.write_record(["hash", "datetime", "name", "repo", "stdout", "stderr"]) + .unwrap(); + + for record in csv { + wtr.write_record(&record).unwrap(); + } + + wtr.flush().unwrap(); + } + } + } else { + eprintln!("Repository is not clean. If you want to allow operating over an unclean repository, pass the `--allow-dirty` flag."); + } +}