Skip to content

Take rebase options from command-line #156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ memchr = "2.3"
anyhow = "1.0"

[dev-dependencies]
current_dir = "0.1.0"
tempfile = "3.1"
10 changes: 8 additions & 2 deletions Documentation/git-absorb.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ FLAGS

-r::
--and-rebase::
Run rebase if successful
Run rebase if successful.
See also the REBASE_OPTIONS below.

-n::
--dry-run::
Expand All @@ -62,7 +63,7 @@ FLAGS

-f::
--force::
Skip all safety checks as if all --force-* flags were givenj
Skip all safety checks as if all --force-* flags were given.
See those flags to understand the full effect of supplying --force.

-w::
Expand Down Expand Up @@ -93,6 +94,11 @@ OPTIONS
Generate completions
[possible values: bash, fish, nushell, zsh, powershell, elvish]

-- <REBASE_OPTIONS>::
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.

USAGE
-----

Expand Down
105 changes: 102 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub struct Config<'a> {
pub force_detach: bool,
pub base: Option<&'a str>,
pub and_rebase: bool,
pub rebase_options: &'a Vec<&'a str>,
pub whole_file: bool,
pub one_fixup_per_commit: bool,
}
Expand All @@ -27,6 +28,12 @@ pub fn run(logger: &slog::Logger, config: &Config) -> Result<()> {
}

fn run_with_repo(logger: &slog::Logger, config: &Config, repo: &git2::Repository) -> Result<()> {
if !config.rebase_options.is_empty() && !config.and_rebase {
return Err(anyhow!(
"REBASE_OPTIONS were specified without --and-rebase flag"
));
}

let config = config::unify(&config, repo);
let stack = stack::working_stack(
repo,
Expand Down Expand Up @@ -367,6 +374,10 @@ fn run_with_repo(logger: &slog::Logger, config: &Config, repo: &git2::Repository
let mut command = Command::new("git");
command.args(["rebase", "--interactive", "--autosquash", "--autostash"]);

for arg in config.rebase_options {
command.arg(arg);
}

if number_of_parents == 0 {
command.arg("--root");
} else {
Expand Down Expand Up @@ -480,6 +491,7 @@ fn index_stats(repo: &git2::Repository) -> Result<git2::DiffStats> {

#[cfg(test)]
mod tests {
use git2::message_trailers_strs;
use std::path::PathBuf;

use super::*;
Expand Down Expand Up @@ -525,7 +537,7 @@ mod tests {
fn foreign_author() {
let ctx = repo_utils::prepare_and_stage();

repo_utils::become_new_author(&ctx.repo);
repo_utils::become_author(&ctx.repo, "nobody2", "nobody2@example.com");

// run 'git-absorb'
let drain = slog::Discard;
Expand All @@ -543,7 +555,7 @@ mod tests {
fn foreign_author_with_force_author_flag() {
let ctx = repo_utils::prepare_and_stage();

repo_utils::become_new_author(&ctx.repo);
repo_utils::become_author(&ctx.repo, "nobody2", "nobody2@example.com");

// run 'git-absorb'
let drain = slog::Discard;
Expand All @@ -565,7 +577,7 @@ mod tests {
fn foreign_author_with_force_author_config() {
let ctx = repo_utils::prepare_and_stage();

repo_utils::become_new_author(&ctx.repo);
repo_utils::become_author(&ctx.repo, "nobody2", "nobody2@example.com");

repo_utils::set_config_flag(&ctx.repo, "absorb.forceAuthor");

Expand Down Expand Up @@ -663,6 +675,92 @@ mod tests {
assert!(nothing_left_in_index(&ctx.repo).unwrap());
}

#[test]
fn and_rebase_flag() {
let ctx = repo_utils::prepare_and_stage();
repo_utils::set_config_option(&ctx.repo, "core.editor", "true");

// run 'git-absorb'
let drain = slog::Discard;
let logger = slog::Logger::root(drain, o!());
let config = Config {
and_rebase: true,
..DEFAULT_CONFIG
};
repo_utils::run_in_repo(&ctx, || run_with_repo(&logger, &config, &ctx.repo)).unwrap();

let mut revwalk = ctx.repo.revwalk().unwrap();
revwalk.push_head().unwrap();

assert_eq!(revwalk.count(), 1);
assert!(nothing_left_in_index(&ctx.repo).unwrap());
}

#[test]
fn and_rebase_flag_with_rebase_options() {
let ctx = repo_utils::prepare_and_stage();
repo_utils::set_config_option(&ctx.repo, "core.editor", "true");

// run 'git-absorb'
let drain = slog::Discard;
let logger = slog::Logger::root(drain, o!());
let config = Config {
and_rebase: true,
rebase_options: &vec!["--signoff"],
..DEFAULT_CONFIG
};
repo_utils::run_in_repo(&ctx, || run_with_repo(&logger, &config, &ctx.repo)).unwrap();

let mut revwalk = ctx.repo.revwalk().unwrap();
revwalk.push_head().unwrap();
assert_eq!(revwalk.count(), 1);

let trailers = message_trailers_strs(
ctx.repo
.head()
.unwrap()
.peel_to_commit()
.unwrap()
.message()
.unwrap(),
)
.unwrap();
assert_eq!(
trailers
.iter()
.filter(|trailer| trailer.0 == "Signed-off-by")
.count(),
1
);

assert!(nothing_left_in_index(&ctx.repo).unwrap());
}

#[test]
fn rebase_options_without_and_rebase_flag() {
let ctx = repo_utils::prepare_and_stage();

// run 'git-absorb'
let drain = slog::Discard;
let logger = slog::Logger::root(drain, o!());
let config = Config {
rebase_options: &vec!["--some-option"],
..DEFAULT_CONFIG
};
let result = run_with_repo(&logger, &config, &ctx.repo);

assert_eq!(
result.err().unwrap().to_string(),
"REBASE_OPTIONS were specified without --and-rebase flag"
);

let mut revwalk = ctx.repo.revwalk().unwrap();
revwalk.push_head().unwrap();
assert_eq!(revwalk.count(), 1);
let is_something_in_index = !nothing_left_in_index(&ctx.repo).unwrap();
assert!(is_something_in_index);
}

fn autostage_common(ctx: &repo_utils::Context, file_path: &PathBuf) -> (PathBuf, PathBuf) {
// 1 modification w/o staging
let path = ctx.join(&file_path);
Expand Down Expand Up @@ -788,6 +886,7 @@ mod tests {
force_detach: false,
base: None,
and_rebase: false,
rebase_options: &Vec::new(),
whole_file: false,
one_fixup_per_commit: false,
};
Expand Down
6 changes: 6 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ struct Cli {
/// Run rebase if successful
#[clap(long, short = 'r')]
and_rebase: bool,
/// Extra arguments to pass to git rebase. Only valid if --and-rebase is set
#[clap(last = true)]
rebase_options: Vec<String>,
/// Generate completions
#[clap(long, value_name = "SHELL", value_parser = ["bash", "fish", "nushell", "zsh", "powershell", "elvish"])]
gen_completions: Option<String>,
Expand All @@ -52,6 +55,7 @@ fn main() {
force,
verbose,
and_rebase,
rebase_options,
gen_completions,
whole_file,
one_fixup_per_commit,
Expand Down Expand Up @@ -93,6 +97,7 @@ fn main() {
));
}

let rebase_options: Vec<&str> = rebase_options.iter().map(AsRef::as_ref).collect();
if let Err(e) = git_absorb::run(
&logger,
&git_absorb::Config {
Expand All @@ -101,6 +106,7 @@ fn main() {
force_detach: force_detach || force,
base: base.as_deref(),
and_rebase,
rebase_options: &rebase_options,
whole_file,
one_fixup_per_commit,
},
Expand Down
33 changes: 26 additions & 7 deletions src/tests/repo_utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#[cfg(test)]
use anyhow::Result;
use current_dir::Cwd;
use std::path::{Path, PathBuf};
pub struct Context {
pub repo: git2::Repository,
Expand All @@ -15,6 +17,7 @@ impl Context {
pub fn prepare_repo() -> (Context, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let repo = git2::Repository::init(dir.path()).unwrap();
become_author(&repo, "nobody", "nobody@example.com");

let path = PathBuf::from("test-file.txt");
std::fs::write(
Expand All @@ -32,10 +35,7 @@ lines
// 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();
let signature = repo.signature().unwrap();
repo.commit(
Some("HEAD"),
&signature,
Expand Down Expand Up @@ -76,10 +76,29 @@ pub fn prepare_and_stage() -> Context {
ctx
}

pub fn become_new_author(repo: &git2::Repository) {
/// Set the named repository config option to value.
pub fn set_config_option(repo: &git2::Repository, name: &str, value: &str) {
repo.config().unwrap().set_str(name, value).unwrap();
}

/// Run a function while in the working directory of the repository.
///
/// Can be used to ensure that at most one test changes the working
/// directory at a time, preventing clashes.
pub fn run_in_repo<F>(ctx: &Context, f: F) -> Result<()>
where
F: FnOnce() -> Result<()>,
{
let mut locked_cwd = Cwd::mutex().lock().unwrap();
locked_cwd.set(ctx.dir.path()).unwrap();
f()
}

/// Become a new author - set the user.name and user.email config options.
pub fn become_author(repo: &git2::Repository, name: &str, email: &str) {
let mut config = repo.config().unwrap();
config.set_str("user.name", "nobody2").unwrap();
config.set_str("user.email", "nobody2@example.com").unwrap();
config.set_str("user.name", name).unwrap();
config.set_str("user.email", email).unwrap();
}

/// Detach HEAD from the current branch.
Expand Down