History

fix(gix-tix): avoid pins during rebase continuation

Record externally amended conflict resolutions in the rewrite mapping and preserve that mapping across intermediate materialization. Use it for final continuation checkout so superseded resolution commits are recognized as rewritten instead of pinned as departures.

Codex committed 2 months ago

Commit f49faf11 parent fdd92c94 files · +83 −53Browse files

Unified · Split

4 files
  1. gix-tix/src/command/rebase.rs +26 −1
  2. gix-tix/src/edit/rebase.rs +14 −3
  3. gix-tix/src/edit/time_travel.rs +20 −30
  4. gix-tix/src/lib.rs +23 −19
gix-tix/src/command/rebase.rs+26−1
@@ -597,7 +597,7 @@ mod tests { · 596 unmodified lines
597597 "the materialized conflict commit records the ours tree"
598598 );
599599
600− std::fs::write(fixture.path().join("file"), b"base\n")?;
600+ std::fs::write(fixture.path().join("file"), b"resolved first\n")?;
601601 assert!(
602602 Command::new("git")
603603 .arg("-C")
@@ -606,10 +606,20 @@ mod tests { · 2 unmodified lines
606606 .status()?
607607 .success()
608608 );
609+ assert!(
610+ Command::new("git")
611+ .arg("-C")
612+ .arg(fixture.path())
613+ .args(["commit", "-q", "--amend", "--no-edit"])
614+ .status()?
615+ .success(),
616+ "resolving through the CLI may amend the materialized conflict before continuing"
617+ );
609618 let repo = crate::test_repository::open_with(
610619 fixture.path(),
611620 ["user.name=todo author", "user.email=todo@example.com"],
612621 )?;
622+ let first_resolution = repo.head_id()?.detach();
613623 let next_output = output_dir.path().join("continue-again.md");
614624 let err = apply_document(repo, &continuation, Some(&next_output))
615625 .expect_err("the descendant conflict stops the continuation");
@@ -617,6 +627,12 @@ mod tests { · 1 unmodified line
617627 format!("{err:#}").contains("materialized conflict"),
618628 "the second conflict is materialized: {err:#}"
619629 );
630+ assert!(
631+ crate::history::all_pins(&crate::test_repository::open(fixture.path())?)?
632+ .iter()
633+ .all(|pin| pin.id != first_resolution),
634+ "continuing does not pin the superseded CLI resolution"
635+ );
620636 std::fs::write(fixture.path().join("file"), b"resolved again\n")?;
621637 assert!(
622638 Command::new("git")
@@ -626,6 +642,15 @@ mod tests { · 3 unmodified lines
626642 .status()?
627643 .success()
628644 );
645+ assert!(
646+ Command::new("git")
647+ .arg("-C")
648+ .arg(fixture.path())
649+ .args(["commit", "-q", "--amend", "--no-edit"])
650+ .status()?
651+ .success(),
652+ "the final conflict may also be amended before continuing"
653+ );
629654 let repo = crate::test_repository::open_with(
630655 fixture.path(),
631656 ["user.name=todo author", "user.email=todo@example.com"],
gix-tix/src/edit/rebase.rs+14−3
@@ -1585,6 +1585,7 @@ pub(crate) fn perform_plan_with_progress( · 1584 unmodified lines
15851585 let mut conflict = None;
15861586 let mut marked = false;
15871587 for (index, step) in plan.steps.iter().enumerate() {
1588+ let mut resolved_head = None;
15881589 let parent = match step.parent {
15891590 PlanParent::Existing(id) => {
15901591 repo.find_commit(id).context("could not find a fork target")?;
@@ -1602,11 +1603,13 @@ pub(crate) fn perform_plan_with_progress( · 11 unmodified lines
16021603 .context("could not decode a picked commit")?
16031604 .into_owned()
16041605 .context("could not own a picked commit")?,
1605− PlanCommit::Resolved(_) => {
1606− let mut commit = repo
1606+ PlanCommit::Resolved(planned) => {
1607+ let head = repo
16071608 .head()?
16081609 .peel_to_commit()
1609− .context("could not resolve the conflicted HEAD commit")?
1610+ .context("could not resolve the conflicted HEAD commit")?;
1611+ resolved_head = Some((*planned, head.id));
1612+ let mut commit = head
16101613 .decode()
16111614 .context("could not decode the conflicted HEAD commit")?
16121615 .into_owned()
@@ -1778,6 +1781,14 @@ pub(crate) fn perform_plan_with_progress( · 165 unmodified lines
17781781 {
17791782 note_rewrites.push((old_id, new_id));
17801783 }
1784+ if let Some((planned, actual)) = resolved_head
1785+ && actual != planned
1786+ {
1787+ rewritten.insert(actual, Some(new_id));
1788+ if actual != new_id {
1789+ note_rewrites.push((actual, new_id));
1790+ }
1791+ }
17811792 for old_id in step.squash.iter().take(applied_squash) {
17821793 rewritten.insert(*old_id, Some(new_id));
17831794 if *old_id != new_id {
gix-tix/src/edit/time_travel.rs+20−30
@@ -143,6 +143,12 @@ pub(crate) fn materialize_plan_conflict_reporting( · 142 unmodified lines
143143 Vec<super::undo::RefChange>,
144144 )> {
145145 let original = conflict.original();
146+ let mapped_head = conflict
147+ .repository()
148+ .head()?
149+ .id()
150+ .map(gix::Id::detach)
151+ .map(|id| (id, conflict.map(id)));
146152 let mut conflict = conflict.into_conflict().persist()?;
147153 let mut ref_changes = std::mem::take(&mut conflict.ref_changes);
148154 let (notice, mut checkout_changes) = move_head_to_reporting(
@@ -152,7 +158,10 @@ pub(crate) fn materialize_plan_conflict_reporting( · 3 unmodified lines
152158 None,
153159 revisions,
154160 include_worktrees,
155− |id| conflict.map(id),
161+ |id| match mapped_head {
162+ Some((head, mapped)) if head == id => mapped,
163+ _ => conflict.map(id),
164+ },
156165 )?;
157166 ref_changes.append(&mut checkout_changes);
158167 let mut deletion_changes = delete_deferred_refs(repository_path, bare, &conflict.deferred_ref_deletions)?;
@@ -178,17 +187,16 @@ pub(crate) fn checkout_without_replay( · 19 unmodified lines
178187 revisions: &[OsString],
179188 include_worktrees: bool,
180189 ) -> Result<Option<String>> {
181− Ok(checkout_without_replay_reporting(repository_path, bare, selected, revisions, include_worktrees)?.0)
182−}
183−
184−pub(crate) fn checkout_without_replay_reporting(
185− repository_path: &Path,
186− bare: bool,
187− selected: ObjectId,
188− revisions: &[OsString],
189− include_worktrees: bool,
190−) -> Result<(Option<String>, Vec<super::undo::RefChange>)> {
191− move_head(repository_path, bare, selected, revisions, include_worktrees)
190+ Ok(move_head_to_reporting(
191+ repository_path,
192+ bare,
193+ selected,
194+ None,
195+ revisions,
196+ include_worktrees,
197+ Some,
198+ )?
199+ .0)
192200 }
193201
194202 #[cfg(test)]
@@ -278,24 +286,6 @@ pub(crate) fn checkout_plan_reporting( · 83 unmodified lines
278286 Ok((notice, ref_changes))
279287 }
280288
281−fn move_head(
282− repository_path: &Path,
283− bare: bool,
284− selected: ObjectId,
285− revisions: &[OsString],
286− include_worktrees: bool,
287−) -> Result<(Option<String>, Vec<super::undo::RefChange>)> {
288− move_head_to_reporting(
289− repository_path,
290− bare,
291− selected,
292− None,
293− revisions,
294− include_worktrees,
295− Some,
296− )
297−}
298−
299289 #[cfg(test)]
300290 fn move_head_to<F>(
301291 repository_path: &Path,
gix-tix/src/lib.rs+23−19
@@ -2087,30 +2087,30 @@ fn event_loop( · 2086 unmodified lines
20872087 })();
20882088 match result {
20892089 Ok(edit::rebase::PlanPerform::Complete(outcome)) => {
2090− let notice = outcome
2091− .selected
2092− .map(|selected| {
2093− edit::time_travel::checkout_without_replay_reporting(
2094− &repository_path,
2095− repository_is_bare,
2096− selected,
2097− &revisions,
2098− false,
2099− )
2100− })
2101− .transpose()
2102− .map(|result| result.unwrap_or((None, Vec::new())));
2090+ let checkout = if outcome.selected.is_some() {
2091+ edit::time_travel::checkout_plan_reporting(
2092+ &repository_path,
2093+ repository_is_bare,
2094+ &outcome,
2095+ &revisions,
2096+ false,
2097+ )
2098+ } else {
2099+ Ok((None, outcome.ref_changes.clone()))
2100+ };
21032101 app.clear_rebase_conflict();
21042102 app.clear_rebase_continuation();
21052103 app.set_worktree_conflicted(false);
21062104 let mut changes = std::mem::take(&mut pending_todo_ref_changes);
2107− changes.extend(outcome.ref_changes.iter().cloned());
2108− let message = match notice {
2109− Ok((notice, mut checkout_changes)) => {
2110− changes.append(&mut checkout_changes);
2105+ let message = match checkout {
2106+ Ok((notice, mut outcome_changes)) => {
2107+ changes.append(&mut outcome_changes);
21112108 notice.unwrap_or_else(|| "rebased history".into())
21122109 }
2113− Err(err) => format!("rebase applied, checkout failed: {err:#}"),
2110+ Err(err) => {
2111+ changes.extend(outcome.ref_changes.iter().cloned());
2112+ format!("rebase applied, checkout failed: {err:#}")
2113+ }
21142114 };
21152115 leave_recorded_success(
21162116 &mut app,
@@ -8694,7 +8694,7 @@ mod tests { · 6577 unmodified lines
86948694 let graph = HistoryGraph::for_commits(&repository, &plan.scope)?;
86958695 let outcome = edit::rebase::perform_plan(&repository, &graph, plan)?.complete()?;
86968696 let resolved = outcome.map(head).context("the conflicted commit is retained")?;
8697− edit::time_travel::checkout_without_replay(fixture.path(), false, resolved, &[], false)?;
8697+ edit::time_travel::checkout_plan(fixture.path(), false, &outcome, &[], false)?;
86988698
86998699 let repository = test_repository::open(fixture.path())?;
87008700 assert_eq!(
@@ -8730,6 +8730,10 @@ mod tests { · 29 unmodified lines
87308730 .all(|entry| entry.stage() == gix::index::entry::Stage::Unconflicted),
87318731 "the final index has no conflict stages"
87328732 );
8733+ assert!(
8734+ history::all_pins(&repository)?.iter().all(history::Pin::is_head),
8735+ "continuing a resolved plan creates no ordinary pin for its superseded conflict checkout"
8736+ );
87338737 assert!(
87348738 !git(&["ls-files", "--error-unmatch", "unrelated"])?.status.success(),
87358739 "unrelated unstaged paths remain untouched"

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t