Skip to content

Commit 504d600

Browse files
authored
Merge pull request #155 from blairconrad/force-detach
Add --force-detach flag to bypass detached HEAD check
2 parents f87c60b + b0d70c3 commit 504d600

File tree

7 files changed

+138
-34
lines changed

7 files changed

+138
-34
lines changed

Documentation/git-absorb.adoc

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,17 @@ FLAGS
5353
--force-author::
5454
Generate fixups to commits not made by you
5555

56+
--force-detach::
57+
Generate fixups even when on a non-branch (detached) HEAD
58+
5659
-F::
5760
--one-fixup-per-commit::
5861
Only generate one fixup per commit
5962

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

6568
-w::
6669
--whole-file::
@@ -187,6 +190,18 @@ edit your local or global `.gitconfig` and add the following section:
187190
forceAuthor = true
188191
.............................................................................
189192

193+
GENERATE FIXUPS ON DETACHED HEAD
194+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
195+
196+
By default, git-absorb will not generate fixup commits when HEAD is not a
197+
branch ("is detached"). To always generate fixups on detached HEADs,
198+
edit your local or global `.gitconfig` and add the following section:
199+
200+
.............................................................................
201+
[absorb]
202+
forceDetach = true
203+
.............................................................................
204+
190205
GITHUB PROJECT
191206
--------------
192207

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ or the git-absorb manual page.
102102
## TODO
103103

104104
- implement remote default branch check
105-
- add smaller force flags to disable individual safety checks
106105
- stop using `failure::err_msg` and ensure all error output is actionable by the user
107106
- slightly more log output in the success case
108107
- more tests (esp main module and integration tests)

src/config.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ pub const MAX_STACK: usize = 10;
77
pub const FORCE_AUTHOR_CONFIG_NAME: &str = "absorb.forceAuthor";
88
pub const FORCE_AUTHOR_DEFAULT: bool = false;
99

10+
pub const FORCE_DETACH_CONFIG_NAME: &str = "absorb.forceDetach";
11+
pub const FORCE_DETACH_DEFAULT: bool = false;
12+
1013
pub const ONE_FIXUP_PER_COMMIT_CONFIG_NAME: &str = "absorb.oneFixupPerCommit";
1114
pub const ONE_FIXUP_PER_COMMIT_DEFAULT: bool = false;
1215

@@ -34,8 +37,9 @@ pub fn unify<'config>(config: &'config Config, repo: &Repository) -> Config<'con
3437
ONE_FIXUP_PER_COMMIT_DEFAULT,
3538
),
3639
force_author: config.force_author
37-
|| config.force
3840
|| bool_value(&repo, FORCE_AUTHOR_CONFIG_NAME, FORCE_AUTHOR_DEFAULT),
41+
force_detach: config.force_detach
42+
|| bool_value(&repo, FORCE_DETACH_CONFIG_NAME, FORCE_DETACH_DEFAULT),
3943
..*config
4044
}
4145
}

src/lib.rs

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::io::Write;
1212
pub struct Config<'a> {
1313
pub dry_run: bool,
1414
pub force_author: bool,
15-
pub force: bool,
15+
pub force_detach: bool,
1616
pub base: Option<&'a str>,
1717
pub and_rebase: bool,
1818
pub whole_file: bool,
@@ -28,9 +28,13 @@ pub fn run(logger: &slog::Logger, config: &Config) -> Result<()> {
2828

2929
fn run_with_repo(logger: &slog::Logger, config: &Config, repo: &git2::Repository) -> Result<()> {
3030
let config = config::unify(&config, repo);
31-
// have force flag enable all force* flags
32-
33-
let stack = stack::working_stack(repo, config.base, config.force_author, config.force, logger)?;
31+
let stack = stack::working_stack(
32+
repo,
33+
config.base,
34+
config.force_author,
35+
config.force_detach,
36+
logger,
37+
)?;
3438
if stack.is_empty() {
3539
crit!(logger, "No commits available to fix up, exiting");
3640
return Ok(());
@@ -521,7 +525,7 @@ mod tests {
521525
fn foreign_author() {
522526
let ctx = repo_utils::prepare_and_stage();
523527

524-
repo_utils::become_new_author(&ctx);
528+
repo_utils::become_new_author(&ctx.repo);
525529

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

542-
repo_utils::become_new_author(&ctx);
546+
repo_utils::become_new_author(&ctx.repo);
543547

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

560564
#[test]
561-
fn foreign_author_with_force_flag() {
565+
fn foreign_author_with_force_author_config() {
562566
let ctx = repo_utils::prepare_and_stage();
563567

564-
repo_utils::become_new_author(&ctx);
568+
repo_utils::become_new_author(&ctx.repo);
569+
570+
repo_utils::set_config_flag(&ctx.repo, "absorb.forceAuthor");
571+
572+
// run 'git-absorb'
573+
let drain = slog::Discard;
574+
let logger = slog::Logger::root(drain, o!());
575+
run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo).unwrap();
576+
577+
let mut revwalk = ctx.repo.revwalk().unwrap();
578+
revwalk.push_head().unwrap();
579+
assert_eq!(revwalk.count(), 3);
580+
581+
assert!(nothing_left_in_index(&ctx.repo).unwrap());
582+
}
583+
584+
#[test]
585+
fn detached_head() {
586+
let ctx = repo_utils::prepare_and_stage();
587+
repo_utils::detach_head(&ctx.repo);
588+
589+
// run 'git-absorb'
590+
let drain = slog::Discard;
591+
let logger = slog::Logger::root(drain, o!());
592+
let result = run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo);
593+
assert_eq!(
594+
result.err().unwrap().to_string(),
595+
"HEAD is not a branch, use --force-detach to override"
596+
);
597+
598+
let mut revwalk = ctx.repo.revwalk().unwrap();
599+
revwalk.push_head().unwrap();
600+
assert_eq!(revwalk.count(), 1);
601+
let is_something_in_index = !nothing_left_in_index(&ctx.repo).unwrap();
602+
assert!(is_something_in_index);
603+
}
604+
605+
#[test]
606+
fn detached_head_pointing_at_branch_with_force_detach_flag() {
607+
let ctx = repo_utils::prepare_and_stage();
608+
repo_utils::detach_head(&ctx.repo);
565609

566610
// run 'git-absorb'
567611
let drain = slog::Discard;
568612
let logger = slog::Logger::root(drain, o!());
569613
let config = Config {
570-
force: true,
614+
force_detach: true,
571615
..DEFAULT_CONFIG
572616
};
573617
run_with_repo(&logger, &config, &ctx.repo).unwrap();
618+
let mut revwalk = ctx.repo.revwalk().unwrap();
619+
revwalk.push_head().unwrap();
620+
621+
assert_eq!(revwalk.count(), 1); // nothing was committed
622+
let is_something_in_index = !nothing_left_in_index(&ctx.repo).unwrap();
623+
assert!(is_something_in_index);
624+
}
574625

626+
#[test]
627+
fn detached_head_with_force_detach_flag() {
628+
let ctx = repo_utils::prepare_and_stage();
629+
repo_utils::detach_head(&ctx.repo);
630+
repo_utils::delete_branch(&ctx.repo, "master");
631+
632+
// run 'git-absorb'
633+
let drain = slog::Discard;
634+
let logger = slog::Logger::root(drain, o!());
635+
let config = Config {
636+
force_detach: true,
637+
..DEFAULT_CONFIG
638+
};
639+
run_with_repo(&logger, &config, &ctx.repo).unwrap();
575640
let mut revwalk = ctx.repo.revwalk().unwrap();
576641
revwalk.push_head().unwrap();
577-
assert_eq!(revwalk.count(), 3);
578642

643+
assert_eq!(revwalk.count(), 3);
579644
assert!(nothing_left_in_index(&ctx.repo).unwrap());
580645
}
581646

582647
#[test]
583-
fn foreign_author_with_force_author_config() {
648+
fn detached_head_with_force_detach_config() {
584649
let ctx = repo_utils::prepare_and_stage();
650+
repo_utils::detach_head(&ctx.repo);
651+
repo_utils::delete_branch(&ctx.repo, "master");
585652

586-
repo_utils::become_new_author(&ctx);
587-
588-
ctx.repo
589-
.config()
590-
.unwrap()
591-
.set_str("absorb.forceAuthor", "true")
592-
.unwrap();
653+
repo_utils::set_config_flag(&ctx.repo, "absorb.forceDetach");
593654

594655
// run 'git-absorb'
595656
let drain = slog::Discard;
596657
let logger = slog::Logger::root(drain, o!());
597658
run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo).unwrap();
598-
599659
let mut revwalk = ctx.repo.revwalk().unwrap();
600660
revwalk.push_head().unwrap();
601-
assert_eq!(revwalk.count(), 3);
602661

662+
assert_eq!(revwalk.count(), 3);
603663
assert!(nothing_left_in_index(&ctx.repo).unwrap());
604664
}
605665

@@ -725,7 +785,7 @@ mod tests {
725785
const DEFAULT_CONFIG: Config = Config {
726786
dry_run: false,
727787
force_author: false,
728-
force: false,
788+
force_detach: false,
729789
base: None,
730790
and_rebase: false,
731791
whole_file: false,

src/main.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ struct Cli {
2020
/// Generate fixups to commits not made by you
2121
#[clap(long)]
2222
force_author: bool,
23-
/// Skip all safety checks; generate fixups to commits not made by you (as if by --force-author) and to non-branch HEADs
23+
/// Generate fixups even when on a non-branch (detached) HEAD
24+
#[clap(long)]
25+
force_detach: bool,
26+
/// Skip all safety checks as if all --force-* flags were given
2427
#[clap(long, short)]
2528
force: bool,
2629
/// Display more output
@@ -45,6 +48,7 @@ fn main() {
4548
base,
4649
dry_run,
4750
force_author,
51+
force_detach,
4852
force,
4953
verbose,
5054
and_rebase,
@@ -93,8 +97,8 @@ fn main() {
9397
&logger,
9498
&git_absorb::Config {
9599
dry_run,
96-
force_author,
97-
force,
100+
force_author: force_author || force,
101+
force_detach: force_detach || force,
98102
base: base.as_deref(),
99103
and_rebase,
100104
whole_file,

src/stack.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@ pub fn working_stack<'repo>(
88
repo: &'repo git2::Repository,
99
user_provided_base: Option<&str>,
1010
force_author: bool,
11-
force: bool,
11+
force_detach: bool,
1212
logger: &slog::Logger,
1313
) -> Result<Vec<git2::Commit<'repo>>> {
1414
let head = repo.head()?;
1515
debug!(logger, "head found"; "head" => head.name());
1616

1717
if !head.is_branch() {
18-
if !force {
19-
return Err(anyhow!("HEAD is not a branch, use --force to override"));
18+
if !force_detach {
19+
return Err(anyhow!(
20+
"HEAD is not a branch, use --force-detach to override"
21+
));
2022
} else {
2123
warn!(
2224
logger,
23-
"HEAD is not a branch, but --force used to continue."
25+
"HEAD is not a branch, but --force-detach used to continue."
2426
);
2527
}
2628
}

src/tests/repo_utils.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,28 @@ pub fn prepare_and_stage() -> Context {
7676
ctx
7777
}
7878

79-
pub fn become_new_author(ctx: &Context) {
80-
let mut config = ctx.repo.config().unwrap();
79+
pub fn become_new_author(repo: &git2::Repository) {
80+
let mut config = repo.config().unwrap();
8181
config.set_str("user.name", "nobody2").unwrap();
8282
config.set_str("user.email", "[email protected]").unwrap();
8383
}
84+
85+
/// Detach HEAD from the current branch.
86+
pub fn detach_head(repo: &git2::Repository) {
87+
let head = repo.head().unwrap();
88+
let head_commit = head.peel_to_commit().unwrap();
89+
repo.set_head_detached(head_commit.id()).unwrap();
90+
}
91+
92+
/// Delete the named branch from the repository.
93+
pub fn delete_branch(repo: &git2::Repository, branch_name: &str) {
94+
let mut branch = repo
95+
.find_branch(branch_name, git2::BranchType::Local)
96+
.unwrap();
97+
branch.delete().unwrap();
98+
}
99+
100+
/// Set the named repository config flag to true.
101+
pub fn set_config_flag(repo: &git2::Repository, flag_name: &str) {
102+
repo.config().unwrap().set_str(flag_name, "true").unwrap();
103+
}

0 commit comments

Comments
 (0)