Skip to content

Commit c50a8aa

Browse files
committed
feat(amend): add --reparent option
This option rewrites the tree for the current commit while leaving all descendant commits the same. This can be useful when applying formatting or refactoring changes to a given commit for which you would rather adjust the descendant commits manually than try to merge the conflicting changes.
1 parent c63dc51 commit c50a8aa

File tree

4 files changed

+348
-3
lines changed

4 files changed

+348
-3
lines changed

git-branchless/src/commands/amend.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! that are already tracked in the repo. Following the amend,
55
//! the command performs a restack.
66
7+
use std::convert::TryFrom;
78
use std::ffi::OsString;
89
use std::fmt::Write;
910
use std::time::{SystemTime, UNIX_EPOCH};
@@ -25,10 +26,13 @@ use tracing::instrument;
2526

2627
use crate::opts::{MoveOptions, ResolveRevsetOptions};
2728
use lib::core::config::get_restack_preserve_timestamps;
29+
use lib::core::dag::commit_set_to_vec;
2830
use lib::core::effects::Effects;
2931
use lib::core::eventlog::{Event, EventLogDb, EventReplayer};
3032
use lib::core::formatting::Pluralize;
31-
use lib::git::{AmendFastOptions, GitRunInfo, MaybeZeroOid, Repo, ResolvedReferenceInfo};
33+
use lib::git::{
34+
AmendFastOptions, GitRunInfo, MaybeZeroOid, NonZeroOid, Repo, ResolvedReferenceInfo,
35+
};
3236

3337
/// Amends the existing HEAD commit.
3438
#[instrument]
@@ -37,6 +41,7 @@ pub fn amend(
3741
git_run_info: &GitRunInfo,
3842
resolve_revset_options: &ResolveRevsetOptions,
3943
move_options: &MoveOptions,
44+
reparent: bool,
4045
) -> eyre::Result<ExitCode> {
4146
let now = SystemTime::now();
4247
let timestamp = now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs_f64();
@@ -46,7 +51,7 @@ pub fn amend(
4651
let event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?;
4752
let event_cursor = event_replayer.make_default_cursor();
4853
let references_snapshot = repo.get_references_snapshot()?;
49-
let dag = Dag::open_and_sync(
54+
let mut dag = Dag::open_and_sync(
5055
effects,
5156
&repo,
5257
&event_replayer,
@@ -76,6 +81,21 @@ pub fn amend(
7681
return Ok(ExitCode(1));
7782
}
7883

84+
let build_options = BuildRebasePlanOptions {
85+
force_rewrite_public_commits: move_options.force_rewrite_public_commits,
86+
dump_rebase_constraints: move_options.dump_rebase_constraints,
87+
dump_rebase_plan: move_options.dump_rebase_plan,
88+
detect_duplicate_commits_via_patch_id: move_options.detect_duplicate_commits_via_patch_id,
89+
};
90+
let commits_to_verify = dag.query().descendants(CommitSet::from(head_oid))?;
91+
let commits_to_verify = dag.filter_visible_commits(commits_to_verify)?;
92+
if let Err(err) =
93+
RebasePlanPermissions::verify_rewrite_set(&dag, &build_options, &commits_to_verify)?
94+
{
95+
err.describe(effects, &repo)?;
96+
return Ok(ExitCode(1));
97+
};
98+
7999
let event_tx_id = event_log_db.make_transaction_id(now, "amend")?;
80100
let (snapshot, status) =
81101
repo.get_status(effects, git_run_info, &index, &head_info, Some(event_tx_id))?;
@@ -146,6 +166,12 @@ pub fn amend(
146166
)?;
147167
mark_commit_reachable(&repo, amended_commit_oid)
148168
.wrap_err("Marking commit as reachable for GC purposes.")?;
169+
dag.sync_from_oids(
170+
effects,
171+
&repo,
172+
CommitSet::empty(),
173+
CommitSet::from(amended_commit_oid),
174+
)?;
149175

150176
let rebase_plan = {
151177
let build_options = BuildRebasePlanOptions {
@@ -173,6 +199,28 @@ pub fn amend(
173199
builder.move_subtree(head_oid, head_commit.get_parent_oids())?;
174200
builder.replace_commit(head_oid, amended_commit_oid)?;
175201

202+
// To keep the contents of all descendant commits the same, forcibly
203+
// replace the children commits, and then rely on normal patch
204+
// application to apply the rest.
205+
if reparent {
206+
let descendants = dag
207+
.query()
208+
.descendants(CommitSet::from(head_oid))?
209+
.difference(&CommitSet::from(head_oid));
210+
let descendants = dag.filter_visible_commits(descendants)?;
211+
for descendant_oid in commit_set_to_vec(&descendants)? {
212+
let parents = dag.query().parent_names(descendant_oid.into())?;
213+
builder.move_subtree(
214+
descendant_oid,
215+
parents
216+
.into_iter()
217+
.map(NonZeroOid::try_from)
218+
.try_collect()?,
219+
)?;
220+
builder.replace_commit(descendant_oid, descendant_oid)?;
221+
}
222+
}
223+
176224
let thread_pool = ThreadPoolBuilder::new().build()?;
177225
let repo_pool = RepoResource::new_pool(&repo)?;
178226
match builder.build(effects, &thread_pool, &repo_pool)? {

git-branchless/src/commands/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,15 @@ fn do_main_and_drop_locals() -> eyre::Result<i32> {
135135
}
136136

137137
let ExitCode(exit_code) = match command {
138-
Command::Amend { move_options } => amend::amend(
138+
Command::Amend {
139+
move_options,
140+
reparent,
141+
} => amend::amend(
139142
&effects,
140143
&git_run_info,
141144
&ResolveRevsetOptions::default(),
142145
&move_options,
146+
reparent,
143147
)?,
144148

145149
Command::BugReport => bug_report::bug_report(&effects, &git_run_info)?,

git-branchless/src/opts.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ pub enum Command {
177177
/// Options for moving commits.
178178
#[clap(flatten)]
179179
move_options: MoveOptions,
180+
181+
/// Modify the contents of the current HEAD commit, but keep all contents of descendant
182+
/// commits exactly the same (i.e. "reparent" them). This can be useful when applying
183+
/// formatting or refactoring changes.
184+
#[clap(long)]
185+
reparent: bool,
180186
},
181187

182188
/// Gather information about recent operations to upload as part of a bug

0 commit comments

Comments
 (0)