use byte_unit::{Byte, UnitType};
use clap::Parser;
use git2::{Cred, Diff, RemoteCallbacks, Sort};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use serde::Serialize;
use std::{cell::Cell, str::FromStr};
use tempfile::tempdir;
#[derive(Serialize, Clone, Debug)]
struct FileChange {
path: String,
lines_added: u32,
lines_removed: u32,
lines_modified: u32,
hunks_added: u32,
hunks_removed: u32,
hunks_modified: u32,
}
#[derive(Serialize, Debug, Clone)]
enum CommitType {
Normal,
Merge,
}
#[derive(Serialize, Debug)]
struct Commit {
id: String,
repo_url: String,
timestamp: i64,
author_name: String,
author_email: String,
message: String,
r#type: CommitType,
changes: Vec<FileChange>,
}
#[derive(Serialize, Debug)]
struct FlatCommit {
id: String,
repo_url: String,
timestamp: i64,
author_name: String,
author_email: String,
message: String,
r#type: CommitType,
path: String,
lines_added: u32,
lines_removed: u32,
lines_modified: u32,
hunks_added: u32,
hunks_removed: u32,
hunks_modified: u32,
}
fn extract_from_diff(diff: &Diff) -> Result<Vec<FileChange>, git2::Error> {
let mut files: Vec<FileChange> = Vec::new();
let x: Cell<Option<FileChange>> = Cell::new(None);
diff.foreach(
&mut |diff_delta, _s| {
match x.take() {
Some(file_change) => {
files.push(file_change);
}
_ => {}
}
let filename = diff_delta.new_file().path().unwrap().to_str().unwrap();
x.set(Some(FileChange {
path: String::from_str(filename).unwrap(),
lines_added: 0,
lines_removed: 0,
lines_modified: 0,
hunks_added: 0,
hunks_removed: 0,
hunks_modified: 0,
}));
true
},
None,
Some(&mut |_diff_delta, diff_hunk| {
let state = x.take().unwrap();
let updated = match (diff_hunk.old_lines(), diff_hunk.new_lines()) {
(0, _) => FileChange {
hunks_added: state.hunks_added + 1,
..state
},
(_, 0) => FileChange {
hunks_removed: state.hunks_removed + 1,
..state
},
(_, _) => FileChange {
hunks_modified: state.hunks_modified + 1,
..state
},
};
x.set(Some(updated));
true
}),
Some(&mut |_diff_delta, _diff_hunk, diff_line| {
let state = x.take().unwrap();
let updated = match (diff_line.old_lineno(), diff_line.new_lineno()) {
(None, Some(_)) => FileChange {
lines_added: state.lines_added + 1,
..state
},
(Some(_), None) => FileChange {
lines_removed: state.lines_removed + 1,
..state
},
(Some(_), Some(_)) => FileChange {
lines_modified: state.lines_modified + 1,
..state
},
_ => state,
};
x.set(Some(updated));
true
}),
)?;
Ok(files)
}
fn extract_logs(args: &Args) -> Result<(), git2::Error> {
let multiprogress = MultiProgress::new();
let sty = ProgressStyle::with_template(
"[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}",
)
.unwrap()
.progress_chars("#>-");
let progress_client = multiprogress.add(ProgressBar::new(1));
let progress_server = multiprogress.add(ProgressBar::new(1));
let progress_transfer = multiprogress.add(ProgressBar::new(1));
let progress_indexing = multiprogress.add(ProgressBar::new(1));
progress_client.set_style(sty.clone());
progress_client.set_message("Client progress");
progress_server.set_style(sty.clone());
progress_server.set_message("Server progress");
progress_transfer.set_style(sty.clone());
progress_transfer.set_message("Transfer progress");
progress_indexing.set_style(sty.clone());
progress_indexing.set_message("Indexing progress");
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(|_url, username_from_url, allowed_types| {
progress_client.set_message("Authenticating...");
if allowed_types.is_ssh_key() {
Cred::ssh_key_from_agent(username_from_url.unwrap_or("none"))
} else if allowed_types.is_user_pass_plaintext() {
match (
args.plaintext_username.as_ref(),
args.plaintext_password.as_ref(),
) {
(Some(username), Some(password)) => Cred::userpass_plaintext(username, password),
_ => {
eprintln!("Asked for plaintext credentials but none provided - maybe add -U and -P options?");
Cred::default()
}
}
} else {
eprintln!("Unsupported credential type - returning default (which probably will fail)");
Cred::default()
}
});
callbacks.transfer_progress(|progress| {
let received_byte = Byte::from_u128(u128::try_from(progress.received_bytes()).unwrap())
.unwrap()
.get_appropriate_unit(UnitType::Binary);
progress_transfer.set_length(progress.total_objects() as u64);
progress_transfer.set_position(progress.received_objects() as u64);
progress_transfer.set_message(format!("Tranferred {received_byte:.2}"));
progress_indexing.set_length(progress.total_objects() as u64);
progress_indexing.set_position(progress.indexed_objects() as u64);
true
});
callbacks.pack_progress(|pack_builder_stage, current, total| {
eprintln!(
"Packing => Stage {:?}, b {}, c {}",
pack_builder_stage, current, total
);
()
});
callbacks.sideband_progress(|sb| {
let msg = format!(
"Server: {}",
String::from_utf8_lossy(sb)
.to_string()
.replace("\n", "")
.replace("\r", "")
);
progress_server.set_message(msg);
true
});
let mut fo = git2::FetchOptions::new();
fo.remote_callbacks(callbacks);
let mut builder = git2::build::RepoBuilder::new();
builder.fetch_options(fo);
let temp_dir = tempdir().map_err(|_e| git2::Error::from_str("TempDir failed!"))?;
progress_client.set_message("Cloning...");
let repo = builder.clone(args.repo_url.as_str(), &temp_dir.path())?;
progress_client.set_message("Clone complete");
let mut revwalk = repo.revwalk()?;
revwalk.set_sorting(Sort::TIME)?;
revwalk.push_head()?;
progress_client.finish();
progress_server.finish();
progress_transfer.finish();
progress_indexing.finish();
let branches = repo.branches(None)?;
for branch_r in branches {
if let Ok((branch, _branch_type)) = branch_r {
if !branch.is_head() {
if let Some(target) = branch.get().target() {
revwalk.push(target)?;
} else {
}
}
}
}
while let Some(Ok(oid)) = revwalk.next() {
let commit = repo.find_commit(oid)?;
let commit_tree = repo.find_tree(commit.tree_id()).unwrap();
let parent_commit = if commit.parent_count() == 0 {
None
} else {
Some(commit.parent(0).unwrap().tree_id())
};
let parent_tree = parent_commit.map(|oid| repo.find_tree(oid).unwrap());
let default_commit = Commit {
id: oid.to_string(),
r#type: CommitType::Normal,
repo_url: args.repo_url.to_string(),
timestamp: commit.time().seconds(),
author_name: commit.author().name().unwrap_or("unknown").to_string(),
author_email: commit.author().email().unwrap_or("unknown").to_string(),
message: commit.message().unwrap_or("unknown").to_string(),
changes: Vec::new(),
};
let my_commit = if commit.parent_count() > 1 {
Commit {
r#type: CommitType::Merge,
..default_commit
}
}
else {
let diff = repo.diff_tree_to_tree(parent_tree.as_ref(), Some(&commit_tree), None)?;
let file_changes = extract_from_diff(&diff)?;
Commit {
r#type: CommitType::Normal,
changes: file_changes,
..default_commit
}
};
let flat: Vec<FlatCommit> = my_commit
.changes
.iter()
.map(|change| FlatCommit {
id: my_commit.id.clone(),
r#type: my_commit.r#type.clone(),
repo_url: my_commit.repo_url.clone(),
timestamp: my_commit.timestamp.clone(),
author_name: my_commit.author_name.clone(),
author_email: my_commit.author_email.clone(),
message: my_commit.message.clone(),
path: change.path.clone(),
lines_added: change.lines_added.clone(),
lines_removed: change.lines_removed.clone(),
lines_modified: change.lines_modified.clone(),
hunks_added: change.hunks_added.clone(),
hunks_removed: change.hunks_removed.clone(),
hunks_modified: change.hunks_modified.clone(),
})
.collect();
flat.iter().for_each(|f| {
let my_flat_json =
serde_json::to_string(&f).map_err(|_e| git2::Error::from_str("Serde failed!"));
if let Ok(f) = my_flat_json {
println!("{}", f);
}
});
}
Ok(())
}
#[derive(Parser, Debug)]
struct Args {
#[arg(short = 'U', help = "Username to provide for PLAINTEXT auth")]
plaintext_username: Option<String>,
#[arg(short = 'P', help = "Password to provide for PLAINTEXT auth")]
plaintext_password: Option<String>,
#[arg(help = "The URL of the repository to clone")]
repo_url: String,
}
fn main() {
let args = Args::parse();
match extract_logs(&args) {
Ok(()) => {
}
Err(e) => eprintln!("Err {:?}", e),
};
()
}