diff options
author | Stephen Jung <tummychow511@gmail.com> | 2023-07-25 17:52:27 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-07-25 17:52:27 -0400 |
commit | bca5f29ec4014f559aa6ebe8f788a9c51b9e5cbc (patch) | |
tree | e4666ab95d66e9eecb9c1db76741d2a92d9a9a4c | |
parent | d61ed5f635e6e30211c49d9285b807dbfd1507e4 (diff) | |
parent | e16fcff80e899aefbe942e8ee206e2d4cc8bf579 (diff) |
Merge pull request #91 from rbartlensky/one-fixup-per-commit
Add feature to generate one fixup per commit.
-rw-r--r-- | src/lib.rs | 197 | ||||
-rw-r--r-- | src/main.rs | 8 |
2 files changed, 197 insertions, 8 deletions
@@ -14,13 +14,17 @@ pub struct Config<'a> { pub base: Option<&'a str>, pub and_rebase: bool, pub whole_file: bool, + pub one_fixup_per_commit: bool, pub logger: &'a slog::Logger, } pub fn run(config: &Config) -> Result<()> { let repo = git2::Repository::open_from_env()?; debug!(config.logger, "repository found"; "path" => repo.path().to_str()); + run_with_repo(config, &repo) +} +fn run_with_repo(config: &Config, repo: &git2::Repository) -> Result<()> { let stack = stack::working_stack(&repo, config.base, config.force, config.logger)?; if stack.is_empty() { crit!(config.logger, "No commits available to fix up, exiting"); @@ -80,6 +84,8 @@ pub fn run(config: &Config) -> Result<()> { .or_else(|_| git2::Signature::now("nobody", "nobody@example.com"))?; let mut head_commit = repo.head()?.peel_to_commit()?; + let mut hunks_with_commit = vec![]; + let mut patches_considered = 0usize; 'patch: for index_patch in index.iter() { let old_path = index_patch.new_path.as_slice(); @@ -174,7 +180,10 @@ pub fn run(config: &Config) -> Result<()> { // cases, might be helpful to just match the first commit touching the same // file as the current hunk. Use this option with care! if config.whole_file { - debug!(c_logger, "Commit touches the hunk file and match whole file is enabled"); + debug!( + c_logger, + "Commit touches the hunk file and match whole file is enabled" + ); dest_commit = Some(commit); break 'commit; } @@ -223,18 +232,56 @@ pub fn run(config: &Config) -> Result<()> { } }; + let hunk_with_commit = HunkWithCommit { + hunk_to_apply, + dest_commit, + index_patch, + }; + hunks_with_commit.push(hunk_with_commit); + + applied_hunks_offset += hunk_offset; + } + } + + hunks_with_commit.sort_by_key(|h| h.dest_commit.id()); + // * apply all hunks that are going to be fixed up into `dest_commit` + // * commit the fixup + // * repeat for all `dest_commit`s + // + // the `.zip` here will gives us something similar to `.windows`, but with + // an extra iteration for the last element (otherwise we would have to + // special case the last element and commit it separately) + for (current, next) in hunks_with_commit + .iter() + .zip(hunks_with_commit.iter().skip(1).map(Some).chain([None])) + { + head_tree = apply_hunk_to_tree( + &repo, + &head_tree, + ¤t.hunk_to_apply, + ¤t.index_patch.old_path, + )?; + + // whether there are no more hunks to apply to `dest_commit` + let commit_fixup = next.map_or(true, |next| { + // if the next hunk is for a different commit -- commit what we have so far + !config.one_fixup_per_commit || next.dest_commit.id() != current.dest_commit.id() + }); + if commit_fixup { // TODO: the git2 api only supports utf8 commit messages, // so it's okay to use strings instead of bytes here // https://docs.rs/git2/0.7.5/src/git2/repo.rs.html#998 // https://libgit2.org/libgit2/#HEAD/group/commit/git_commit_create - let dest_commit_id = dest_commit.id().to_string(); - let dest_commit_locator = dest_commit + let dest_commit_id = current.dest_commit.id().to_string(); + let dest_commit_locator = current + .dest_commit .summary() .filter(|&msg| summary_counts[msg] == 1) .unwrap_or(&dest_commit_id); + let diff = repo + .diff_tree_to_tree(Some(&head_commit.tree()?), Some(&head_tree), None)? + .stats()?; if !config.dry_run { - head_tree = - apply_hunk_to_tree(&repo, &head_tree, &hunk_to_apply, &index_patch.old_path)?; head_commit = repo.find_commit(repo.commit( Some("HEAD"), &signature, @@ -245,15 +292,14 @@ pub fn run(config: &Config) -> Result<()> { )?)?; info!(config.logger, "committed"; "commit" => head_commit.id().to_string(), - "header" => hunk_to_apply.header(), + "header" => format!("+{},-{}", diff.insertions(), diff.deletions()), ); } else { info!(config.logger, "would have committed"; "fixup" => dest_commit_locator, - "header" => hunk_to_apply.header(), + "header" => format!("+{},-{}", diff.insertions(), diff.deletions()), ); } - applied_hunks_offset += hunk_offset; } } @@ -290,6 +336,12 @@ pub fn run(config: &Config) -> Result<()> { Ok(()) } +struct HunkWithCommit<'c, 'r, 'p> { + hunk_to_apply: owned::Hunk, + dest_commit: &'c git2::Commit<'r>, + index_patch: &'p owned::Patch, +} + fn apply_hunk_to_tree<'repo>( repo: &'repo git2::Repository, base: &git2::Tree, @@ -364,3 +416,132 @@ fn split_lines_after(content: &[u8], n: usize) -> (&[u8], &[u8]) { }; content.split_at(split_index) } + +#[cfg(test)] +mod tests { + use std::path::{Path, PathBuf}; + + use super::*; + + struct Context { + repo: git2::Repository, + dir: tempfile::TempDir, + } + + impl Context { + fn join(&self, p: &Path) -> PathBuf { + self.dir.path().join(p) + } + } + + /// Prepare a fresh git repository with an initial commit and a file. + fn prepare_repo() -> (Context, PathBuf) { + let dir = tempfile::tempdir().unwrap(); + let repo = git2::Repository::init(dir.path()).unwrap(); + + let path = PathBuf::from("test-file.txt"); + std::fs::write( + dir.path().join(&path), + br#" +line +line + +more +lines +"#, + ) + .unwrap(); + + // make the borrow-checker happy by introducing a new scope + { + let tree = add(&repo, &path); + let signature = repo + .signature() + .or_else(|_| git2::Signature::now("nobody", "nobody@example.com")) + .unwrap(); + repo.commit( + Some("HEAD"), + &signature, + &signature, + "Initial commit.", + &tree, + &[], + ) + .unwrap(); + } + + (Context { repo, dir }, path) + } + + /// Stage the changes made to `path`. + fn add<'r>(repo: &'r git2::Repository, path: &Path) -> git2::Tree<'r> { + let mut index = repo.index().unwrap(); + index.add_path(&path).unwrap(); + index.write().unwrap(); + + let tree_id = index.write_tree_to(&repo).unwrap(); + repo.find_tree(tree_id).unwrap() + } + + /// Prepare an empty repo, and stage some changes. + fn prepare_and_stage() -> Context { + let (ctx, file_path) = prepare_repo(); + + // add some lines to our file + let path = ctx.join(&file_path); + let contents = std::fs::read_to_string(&path).unwrap(); + let modifications = format!("new_line1\n{contents}\nnew_line2"); + std::fs::write(&path, &modifications).unwrap(); + + // stage it + add(&ctx.repo, &file_path); + + ctx + } + + #[test] + fn multiple_fixups_per_commit() { + let ctx = prepare_and_stage(); + + // run 'git-absorb' + let drain = slog::Discard; + let logger = slog::Logger::root(drain, o!()); + let config = Config { + dry_run: false, + force: false, + base: None, + and_rebase: false, + whole_file: false, + one_fixup_per_commit: false, + logger: &logger, + }; + run_with_repo(&config, &ctx.repo).unwrap(); + + let mut revwalk = ctx.repo.revwalk().unwrap(); + revwalk.push_head().unwrap(); + assert_eq!(revwalk.count(), 3); + } + + #[test] + fn one_fixup_per_commit() { + let ctx = prepare_and_stage(); + + // run 'git-absorb' + let drain = slog::Discard; + let logger = slog::Logger::root(drain, o!()); + let config = Config { + dry_run: false, + force: false, + base: None, + and_rebase: false, + whole_file: false, + one_fixup_per_commit: true, + logger: &logger, + }; + run_with_repo(&config, &ctx.repo).unwrap(); + + let mut revwalk = ctx.repo.revwalk().unwrap(); + revwalk.push_head().unwrap(); + assert_eq!(revwalk.count(), 2); + } +} diff --git a/src/main.rs b/src/main.rs index 7cb83b9..5ee6fef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,13 @@ fn main() { .short("w") .long("whole-file") .takes_value(false), + ) + .arg( + clap::Arg::with_name("one-fixup-per-commit") + .help("Only generate one fixup per commit") + .short("F") + .long("one-fixup-per-commit") + .takes_value(false), ); let mut args_clone = args.clone(); let args = args.get_matches(); @@ -112,6 +119,7 @@ fn main() { base: args.value_of("base"), and_rebase: args.is_present("and-rebase"), whole_file: args.is_present("whole-file"), + one_fixup_per_commit: args.is_present("one-fixup-per-commit"), logger: &logger, }) { crit!(logger, "absorb failed"; "err" => e.to_string()); |