Skip to content
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
19 changes: 17 additions & 2 deletions Documentation/git-absorb.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,17 @@ FLAGS
--force-author::
Generate fixups to commits not made by you

--force-detach::
Generate fixups even when on a non-branch (detached) HEAD

-F::
--one-fixup-per-commit::
Only generate one fixup per commit

-f::
--force::
Skip all safety checks.
Generate fixups to commits not made by you (as if by --force-author) and to non-branch HEADs
Skip all safety checks as if all --force-* flags were givenj
See those flags to understand the full effect of supplying --force.

-w::
--whole-file::
Expand Down Expand Up @@ -187,6 +190,18 @@ edit your local or global `.gitconfig` and add the following section:
forceAuthor = true
.............................................................................

GENERATE FIXUPS ON DETACHED HEAD
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default, git-absorb will not generate fixup commits when HEAD is not a
branch ("is detached"). To always generate fixups on detached HEADs,
edit your local or global `.gitconfig` and add the following section:

.............................................................................
[absorb]
forceDetach = true
.............................................................................

GITHUB PROJECT
--------------

Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ or the git-absorb manual page.
## TODO

- implement remote default branch check
- add smaller force flags to disable individual safety checks
- stop using `failure::err_msg` and ensure all error output is actionable by the user
- slightly more log output in the success case
- more tests (esp main module and integration tests)
Expand Down
6 changes: 5 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ pub const MAX_STACK: usize = 10;
pub const FORCE_AUTHOR_CONFIG_NAME: &str = "absorb.forceAuthor";
pub const FORCE_AUTHOR_DEFAULT: bool = false;

pub const FORCE_DETACH_CONFIG_NAME: &str = "absorb.forceDetach";
pub const FORCE_DETACH_DEFAULT: bool = false;

pub const ONE_FIXUP_PER_COMMIT_CONFIG_NAME: &str = "absorb.oneFixupPerCommit";
pub const ONE_FIXUP_PER_COMMIT_DEFAULT: bool = false;

Expand Down Expand Up @@ -34,8 +37,9 @@ pub fn unify<'config>(config: &'config Config, repo: &Repository) -> Config<'con
ONE_FIXUP_PER_COMMIT_DEFAULT,
),
force_author: config.force_author
|| config.force
|| bool_value(&repo, FORCE_AUTHOR_CONFIG_NAME, FORCE_AUTHOR_DEFAULT),
force_detach: config.force_detach
|| bool_value(&repo, FORCE_DETACH_CONFIG_NAME, FORCE_DETACH_DEFAULT),
..*config
}
}
Expand Down
102 changes: 81 additions & 21 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::io::Write;
pub struct Config<'a> {
pub dry_run: bool,
pub force_author: bool,
pub force: bool,
pub force_detach: bool,
pub base: Option<&'a str>,
pub and_rebase: bool,
pub whole_file: bool,
Expand All @@ -28,9 +28,13 @@ pub fn run(logger: &slog::Logger, config: &Config) -> Result<()> {

fn run_with_repo(logger: &slog::Logger, config: &Config, repo: &git2::Repository) -> Result<()> {
let config = config::unify(&config, repo);
// have force flag enable all force* flags

let stack = stack::working_stack(repo, config.base, config.force_author, config.force, logger)?;
let stack = stack::working_stack(
repo,
config.base,
config.force_author,
config.force_detach,
logger,
)?;
if stack.is_empty() {
crit!(logger, "No commits available to fix up, exiting");
return Ok(());
Expand Down Expand Up @@ -521,7 +525,7 @@ mod tests {
fn foreign_author() {
let ctx = repo_utils::prepare_and_stage();

repo_utils::become_new_author(&ctx);
repo_utils::become_new_author(&ctx.repo);

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

repo_utils::become_new_author(&ctx);
repo_utils::become_new_author(&ctx.repo);

// run 'git-absorb'
let drain = slog::Discard;
Expand All @@ -558,48 +562,104 @@ mod tests {
}

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

repo_utils::become_new_author(&ctx);
repo_utils::become_new_author(&ctx.repo);

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

// run 'git-absorb'
let drain = slog::Discard;
let logger = slog::Logger::root(drain, o!());
run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo).unwrap();

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

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

#[test]
fn detached_head() {
let ctx = repo_utils::prepare_and_stage();
repo_utils::detach_head(&ctx.repo);

// run 'git-absorb'
let drain = slog::Discard;
let logger = slog::Logger::root(drain, o!());
let result = run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo);
assert_eq!(
result.err().unwrap().to_string(),
"HEAD is not a branch, use --force-detach to override"
);

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);
}

#[test]
fn detached_head_pointing_at_branch_with_force_detach_flag() {
let ctx = repo_utils::prepare_and_stage();
repo_utils::detach_head(&ctx.repo);

// run 'git-absorb'
let drain = slog::Discard;
let logger = slog::Logger::root(drain, o!());
let config = Config {
force: true,
force_detach: true,
..DEFAULT_CONFIG
};
run_with_repo(&logger, &config, &ctx.repo).unwrap();
let mut revwalk = ctx.repo.revwalk().unwrap();
revwalk.push_head().unwrap();

assert_eq!(revwalk.count(), 1); // nothing was committed
let is_something_in_index = !nothing_left_in_index(&ctx.repo).unwrap();
assert!(is_something_in_index);
}

#[test]
fn detached_head_with_force_detach_flag() {
let ctx = repo_utils::prepare_and_stage();
repo_utils::detach_head(&ctx.repo);
repo_utils::delete_branch(&ctx.repo, "master");

// run 'git-absorb'
let drain = slog::Discard;
let logger = slog::Logger::root(drain, o!());
let config = Config {
force_detach: true,
..DEFAULT_CONFIG
};
run_with_repo(&logger, &config, &ctx.repo).unwrap();
let mut revwalk = ctx.repo.revwalk().unwrap();
revwalk.push_head().unwrap();
assert_eq!(revwalk.count(), 3);

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

#[test]
fn foreign_author_with_force_author_config() {
fn detached_head_with_force_detach_config() {
let ctx = repo_utils::prepare_and_stage();
repo_utils::detach_head(&ctx.repo);
repo_utils::delete_branch(&ctx.repo, "master");

repo_utils::become_new_author(&ctx);

ctx.repo
.config()
.unwrap()
.set_str("absorb.forceAuthor", "true")
.unwrap();
repo_utils::set_config_flag(&ctx.repo, "absorb.forceDetach");

// run 'git-absorb'
let drain = slog::Discard;
let logger = slog::Logger::root(drain, o!());
run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo).unwrap();

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

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

Expand Down Expand Up @@ -725,7 +785,7 @@ mod tests {
const DEFAULT_CONFIG: Config = Config {
dry_run: false,
force_author: false,
force: false,
force_detach: false,
base: None,
and_rebase: false,
whole_file: false,
Expand Down
10 changes: 7 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ struct Cli {
/// Generate fixups to commits not made by you
#[clap(long)]
force_author: bool,
/// Skip all safety checks; generate fixups to commits not made by you (as if by --force-author) and to non-branch HEADs
/// Generate fixups even when on a non-branch (detached) HEAD
#[clap(long)]
force_detach: bool,
/// Skip all safety checks as if all --force-* flags were given
#[clap(long, short)]
force: bool,
/// Display more output
Expand All @@ -45,6 +48,7 @@ fn main() {
base,
dry_run,
force_author,
force_detach,
force,
verbose,
and_rebase,
Expand Down Expand Up @@ -93,8 +97,8 @@ fn main() {
&logger,
&git_absorb::Config {
dry_run,
force_author,
force,
force_author: force_author || force,
force_detach: force_detach || force,
base: base.as_deref(),
and_rebase,
whole_file,
Expand Down
10 changes: 6 additions & 4 deletions src/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ pub fn working_stack<'repo>(
repo: &'repo git2::Repository,
user_provided_base: Option<&str>,
force_author: bool,
force: bool,
force_detach: bool,
logger: &slog::Logger,
) -> Result<Vec<git2::Commit<'repo>>> {
let head = repo.head()?;
debug!(logger, "head found"; "head" => head.name());

if !head.is_branch() {
if !force {
return Err(anyhow!("HEAD is not a branch, use --force to override"));
if !force_detach {
return Err(anyhow!(
"HEAD is not a branch, use --force-detach to override"
));
} else {
warn!(
logger,
"HEAD is not a branch, but --force used to continue."
"HEAD is not a branch, but --force-detach used to continue."
);
}
}
Expand Down
24 changes: 22 additions & 2 deletions src/tests/repo_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,28 @@ pub fn prepare_and_stage() -> Context {
ctx
}

pub fn become_new_author(ctx: &Context) {
let mut config = ctx.repo.config().unwrap();
pub fn become_new_author(repo: &git2::Repository) {
let mut config = repo.config().unwrap();
config.set_str("user.name", "nobody2").unwrap();
config.set_str("user.email", "nobody2@example.com").unwrap();
}

/// Detach HEAD from the current branch.
pub fn detach_head(repo: &git2::Repository) {
let head = repo.head().unwrap();
let head_commit = head.peel_to_commit().unwrap();
repo.set_head_detached(head_commit.id()).unwrap();
}

/// Delete the named branch from the repository.
pub fn delete_branch(repo: &git2::Repository, branch_name: &str) {
let mut branch = repo
.find_branch(branch_name, git2::BranchType::Local)
.unwrap();
branch.delete().unwrap();
}

/// Set the named repository config flag to true.
pub fn set_config_flag(repo: &git2::Repository, flag_name: &str) {
repo.config().unwrap().set_str(flag_name, "true").unwrap();
}