diff --git a/Documentation/git-absorb.adoc b/Documentation/git-absorb.adoc index d6bce4c..c4e5534 100644 --- a/Documentation/git-absorb.adoc +++ b/Documentation/git-absorb.adoc @@ -66,6 +66,14 @@ FLAGS Skip all safety checks as if all --force-* flags were given. See those flags to understand the full effect of supplying --force. +-s:: +--squash:: + Create squash commits instead of fixup commits. + + + When this flag is used, "fixup commit" may be read as "squash commit" + throughout the documentation. All configuration relating to fixup + commits will apply to the squash commits instead. + -w:: --whole-file:: Match the first commit touching the same file as the current hunk. @@ -98,7 +106,7 @@ OPTIONS Generate completions [possible values: bash, fish, nushell, zsh, powershell, elvish] --- :: +\-- :: Options to pass to git rebase after generating commits. Must be the last arguments and the `--` must be present. Only valid when `--and-rebase` is used. @@ -212,6 +220,22 @@ edit your local or global `.gitconfig` and add the following section: forceDetach = true ............................................................................. +GENERATE SQUASH COMMITS INSTEAD OF FIXUPS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, git-absorb will generate fixup commits. +To instead generate squash commits, edit your local or global `.gitconfig` +and add the following section: + +............................................................................. +[absorb] + createSquashCommits = true +............................................................................. + +When this option is set, "fixup commit" may be read as "squash commit" +throughout the documentation. All configuration relating to fixup +commits will apply to the squash commits instead. + GITHUB PROJECT -------------- diff --git a/src/config.rs b/src/config.rs index 39e576b..fc20e5f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,6 +19,9 @@ pub const AUTO_STAGE_IF_NOTHING_STAGED_DEFAULT: bool = false; pub const FIXUP_TARGET_ALWAYS_SHA_CONFIG_NAME: &str = "absorb.fixupTargetAlwaysSHA"; pub const FIXUP_TARGET_ALWAYS_SHA_DEFAULT: bool = false; +pub const CREATE_SQUASH_COMMITS_CONFIG_NAME: &str = "absorb.createSquashCommits"; +pub const CREATE_SQUASH_COMMITS_DEFAULT: bool = false; + pub fn unify<'config>(config: &'config Config, repo: &Repository) -> Config<'config> { Config { // here, we default to the git config value, @@ -36,6 +39,12 @@ pub fn unify<'config>(config: &'config Config, repo: &Repository) -> Config<'con ONE_FIXUP_PER_COMMIT_CONFIG_NAME, ONE_FIXUP_PER_COMMIT_DEFAULT, ), + squash: config.squash + || bool_value( + repo, + CREATE_SQUASH_COMMITS_CONFIG_NAME, + CREATE_SQUASH_COMMITS_DEFAULT, + ), force_author: config.force_author || bool_value(repo, FORCE_AUTHOR_CONFIG_NAME, FORCE_AUTHOR_DEFAULT), force_detach: config.force_detach diff --git a/src/lib.rs b/src/lib.rs index d821789..156c1b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ pub struct Config<'a> { pub rebase_options: &'a Vec<&'a str>, pub whole_file: bool, pub one_fixup_per_commit: bool, + pub squash: bool, pub message: Option<&'a str>, } @@ -321,7 +322,8 @@ fn run_with_repo(logger: &slog::Logger, config: &Config, repo: &git2::Repository .stats()?; if !config.dry_run { head_tree = new_head_tree; - let mut message = format!("fixup! {}\n", dest_commit_locator); + let verb = if config.squash { "squash" } else { "fixup" }; + let mut message = format!("{}! {}\n", verb, dest_commit_locator); if let Some(m) = config.message.filter(|m| !m.is_empty()) { message.push('\n'); message.push_str(m); @@ -718,6 +720,15 @@ mod tests { let pre_absorb_ref_commit = ctx.repo.refname_to_id("PRE_ABSORB_HEAD").unwrap(); assert_eq!(pre_absorb_ref_commit, actual_pre_absorb_commit); + assert_eq!( + extract_commit_messages(&ctx.repo), + vec![ + "fixup! Initial commit.\n", + "fixup! Initial commit.\n", + "Initial commit.", + ] + ); + log_utils::assert_log_messages_are( capturing_logger.visible_logs(), vec![ @@ -1495,6 +1506,74 @@ mod tests { assert!(is_something_in_index); } + #[test] + fn squash_flag() { + let ctx = repo_utils::prepare_and_stage(); + + // run 'git-absorb' + let mut capturing_logger = log_utils::CapturingLogger::new(); + let config = Config { + squash: true, + ..DEFAULT_CONFIG + }; + run_with_repo(&capturing_logger.logger, &config, &ctx.repo).unwrap(); + + assert_eq!( + extract_commit_messages(&ctx.repo), + vec![ + "squash! Initial commit.\n", + "squash! Initial commit.\n", + "Initial commit.", + ] + ); + + log_utils::assert_log_messages_are( + capturing_logger.visible_logs(), + vec![ + &json!({"level": "INFO", "msg": "committed"}), + &json!({"level": "INFO", "msg": "committed"}), + &json!({ + "level": "INFO", + "msg": "To squash the new commits, rebase:", + "command": "git rebase --interactive --autosquash --autostash --root", + }), + ], + ); + } + + #[test] + fn run_with_squash_config_option() { + let ctx = repo_utils::prepare_and_stage(); + + repo_utils::set_config_flag(&ctx.repo, "absorb.createSquashCommits"); + + // run 'git-absorb' + let mut capturing_logger = log_utils::CapturingLogger::new(); + run_with_repo(&capturing_logger.logger, &DEFAULT_CONFIG, &ctx.repo).unwrap(); + + assert_eq!( + extract_commit_messages(&ctx.repo), + vec![ + "squash! Initial commit.\n", + "squash! Initial commit.\n", + "Initial commit.", + ] + ); + + log_utils::assert_log_messages_are( + capturing_logger.visible_logs(), + vec![ + &json!({"level": "INFO", "msg": "committed"}), + &json!({"level": "INFO", "msg": "committed"}), + &json!({ + "level": "INFO", + "msg": "To squash the new commits, rebase:", + "command": "git rebase --interactive --autosquash --autostash --root", + }), + ], + ); + } + #[test] fn dry_run_flag() { let ctx = repo_utils::prepare_and_stage(); @@ -1814,6 +1893,23 @@ mod tests { assert_eq!(actual_msg, expected_msg); } + /// Perform a revwalk from HEAD, extracting the commit messages. + fn extract_commit_messages(repo: &git2::Repository) -> Vec { + let mut revwalk = repo.revwalk().unwrap(); + revwalk.push_head().unwrap(); + + let mut messages = Vec::new(); + + for oid in revwalk { + let commit = repo.find_commit(oid.unwrap()).unwrap(); + if let Some(message) = commit.message() { + messages.push(message.to_string()); + } + } + + messages + } + const DEFAULT_CONFIG: Config = Config { dry_run: false, force_author: false, @@ -1823,6 +1919,7 @@ mod tests { rebase_options: &Vec::new(), whole_file: false, one_fixup_per_commit: false, + squash: false, message: None, }; } diff --git a/src/main.rs b/src/main.rs index 7a4301c..59e88cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,6 +44,9 @@ struct Cli { /// Only generate one fixup per commit #[clap(long, short = 'F')] one_fixup_per_commit: bool, + /// Create squash commits instead of fixup + #[clap(long, short = 's')] + squash: bool, /// Commit message body that is given to all fixup commits #[clap(long, short)] message: Option, @@ -62,6 +65,7 @@ fn main() { gen_completions, whole_file, one_fixup_per_commit, + squash, message, } = Cli::parse(); @@ -113,6 +117,7 @@ fn main() { rebase_options: &rebase_options, whole_file, one_fixup_per_commit, + squash, message: message.as_deref(), }, ) {