This commit is contained in:
JMARyA 2024-03-09 23:40:36 +01:00
commit 5802b1104c
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
5 changed files with 541 additions and 0 deletions

46
src/args.rs Normal file
View file

@ -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()
}

207
src/main.rs Normal file
View file

@ -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<String> {
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<Vec<Commit>, 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::<String>("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::<String>("command").unwrap()).unwrap()
} else {
args.get_one::<String>("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<Vec<String>> = 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.");
}
}