Skip to content

Commit f6bf9eb

Browse files
committed
Commit reordering: Avoid asking about the same conflict twice
It is fair to assume that the user never wants or expects the sum of changes to be different after reordering them. That would be an impure reordering, which is not what the user is asking for. Even if the conflicts are resolved by the user or git-rerere, it is fair to assume such a result to be a failure. This commit simply always upholds this invariant for the practical benefit of not bothering the user about a conflict with a known resolution. I can hear critics say that this is an assumption, and it's safer not to assume. The answer to that is plainly no: There is only one correct solution to the second conflict given the first. Not just in theory, but when you do this exercise day in and day out and see that the second conflict is always a rearrangement of the first, you want to spend your time on making the first one right. Implements #132
1 parent 4ffe13f commit f6bf9eb

File tree

5 files changed

+49
-36
lines changed

5 files changed

+49
-36
lines changed

gitrevise/merge.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ class MergeConflict(Exception):
2929
pass
3030

3131

32-
def rebase(commit: Commit, new_parent: Optional[Commit]) -> Commit:
32+
def rebase(
33+
commit: Commit,
34+
new_parent: Optional[Commit],
35+
tree_to_keep: Optional[Tree] = None,
36+
) -> Commit:
3337
repo = commit.repo
3438

3539
orig_parent = commit.parent() if not commit.is_root else None
@@ -43,13 +47,16 @@ def get_summary(cmt: Optional[Commit]) -> str:
4347
def get_tree(cmt: Optional[Commit]) -> Tree:
4448
return cmt.tree() if cmt is not None else Tree(repo, b"")
4549

46-
tree = merge_trees(
47-
Path(),
48-
(get_summary(new_parent), get_summary(orig_parent), get_summary(commit)),
49-
get_tree(new_parent),
50-
get_tree(orig_parent),
51-
get_tree(commit),
52-
)
50+
if tree_to_keep is not None:
51+
tree = tree_to_keep
52+
else:
53+
tree = merge_trees(
54+
Path(),
55+
(get_summary(new_parent), get_summary(orig_parent), get_summary(commit)),
56+
get_tree(new_parent),
57+
get_tree(orig_parent),
58+
get_tree(commit),
59+
)
5360

5461
new_parents = [new_parent] if new_parent is not None else []
5562

gitrevise/odb.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -616,12 +616,16 @@ def summary(self) -> str:
616616
)
617617
return " ".join(summary_paragraph.splitlines())
618618

619-
def rebase(self, parent: Optional[Commit]) -> Commit:
619+
def rebase(
620+
self,
621+
parent: Optional[Commit],
622+
tree_to_keep: Optional[Tree] = None,
623+
) -> Commit:
620624
"""Create a new commit with the same changes, except with ``parent``
621625
as its parent. If ``parent`` is ``None``, this becomes a root commit."""
622626
from .merge import rebase # pylint: disable=import-outside-toplevel
623627

624-
return rebase(self, parent)
628+
return rebase(self, parent, tree_to_keep)
625629

626630
def update(
627631
self,

gitrevise/todo.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,23 @@ def edit_todos(
244244

245245
def apply_todos(
246246
current: Optional[Commit],
247-
todos: List[Step],
247+
todos_original: List[Step],
248+
todos_edited: List[Step],
248249
reauthor: bool = False,
249250
) -> Commit:
250-
for step in todos:
251-
rebased = step.commit.rebase(current).update(message=step.message)
251+
applied_old_commits = set()
252+
applied_new_commits = set()
253+
254+
for known_state, step in zip(todos_original, todos_edited):
255+
# Avoid making the user resolve the same conflict twice:
256+
# When reordering commits, the final state is known.
257+
applied_old_commits.add(known_state.commit.oid)
258+
applied_new_commits.add(step.commit.oid)
259+
deja_vu = applied_new_commits == applied_old_commits
260+
tree_to_keep = known_state.commit.tree() if deja_vu else None
261+
262+
rebased = step.commit.rebase(current, tree_to_keep).update(message=step.message)
263+
252264
if step.kind == StepKind.PICK:
253265
current = rebased
254266
elif step.kind == StepKind.FIXUP:

gitrevise/tui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def interactive(
138138

139139
if todos != original:
140140
# Perform the todo list actions.
141-
new_head = apply_todos(base, todos, reauthor=args.reauthor)
141+
new_head = apply_todos(base, original, todos, reauthor=args.reauthor)
142142

143143
# Update the value of HEAD to the new state.
144144
update_head(head, new_head, None)

tests/test_rerere.py

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,10 @@ def test_reuse_recorded_resolution(
3737
history_with_two_conflicting_commits(auto_update=auto_update)
3838

3939
# Uncached case: Record the user's resolution (in .git/rr-cache/*/preimage).
40-
with editor_main(("-i", "HEAD~~"), input=b"y\n" * 4) as ed:
40+
with editor_main(("-i", "HEAD~~"), input=b"y\n" * 2) as ed:
4141
flip_last_two_commits(repo, ed)
4242
with ed.next_file() as f:
4343
f.replace_dedent("spam\n")
44-
with ed.next_file() as f:
45-
f.replace_dedent("eggs spam\n")
4644

4745
tree_after_resolving_conflicts = repo.get_commit("HEAD").tree()
4846
bash("git reset --hard HEAD@{1}")
@@ -51,18 +49,16 @@ def test_reuse_recorded_resolution(
5149
acceptance_input = None
5250
intermediate_state = "spam"
5351
if not auto_update:
54-
acceptance_input = b"y\n" * 2
52+
acceptance_input = b"y\n"
5553
if custom_resolution is not None:
56-
acceptance_input = b"n\n" + b"y\n" * 4
54+
acceptance_input = b"n\n" + b"y\n" * 2
5755
intermediate_state = custom_resolution
5856

5957
with editor_main(("-i", "HEAD~~"), input=acceptance_input) as ed:
6058
flip_last_two_commits(repo, ed)
6159
if custom_resolution is not None:
6260
with ed.next_file() as f:
6361
f.replace_dedent(custom_resolution + "\n")
64-
with ed.next_file() as f:
65-
f.replace_dedent("eggs spam\n")
6662

6763
assert tree_after_resolving_conflicts == repo.get_commit("HEAD").tree()
6864

@@ -96,12 +92,10 @@ def test_rerere_merge(repo: Repository) -> None:
9692
bash("git commit -am 'commit 2'")
9793

9894
# Record a resolution for changing the order of two commits.
99-
with editor_main(("-i", "HEAD~~"), input=b"y\ny\ny\ny\n") as ed:
95+
with editor_main(("-i", "HEAD~~"), input=b"y\ny\n") as ed:
10096
flip_last_two_commits(repo, ed)
10197
with ed.next_file() as f:
10298
f.replace_dedent(b"resolved1\n" + 9 * b"x\n")
103-
with ed.next_file() as f:
104-
f.replace_dedent(b"resolved2\n" + 9 * b"x\n")
10599
# Go back to the old history so we can try replaying the resolution.
106100
bash("git reset --hard HEAD@{1}")
107101

@@ -124,15 +118,9 @@ def test_rerere_merge(repo: Repository) -> None:
124118
"""\
125119
@@ -1 +1 @@
126120
-resolved1
127-
+resolved2"""
128-
)
129-
leftover_index = hunks(repo.git("diff", "-U0", "HEAD"))
130-
assert leftover_index == dedent(
131-
"""\
132-
@@ -1 +1 @@
133-
-resolved2
134-
+original2"""
121+
+original2"""
135122
)
123+
assert uncommitted_changes(repo) == ""
136124

137125

138126
def test_replay_resolution_recorded_by_git(repo: Repository) -> None:
@@ -154,25 +142,27 @@ def test_replay_resolution_recorded_by_git(repo: Repository) -> None:
154142
"""
155143
)
156144

157-
# Now let's try to do the same thing with git-revise, reusing the recorded resolution.
145+
# Test reusing the same recorded resolutions with git-revise.
146+
# Except the last, that is: The total diff, hence the final state,
147+
# of any reordering is a holy invariant.
158148
with editor_main(("-i", "HEAD~~")) as ed:
159149
flip_last_two_commits(repo, ed)
160150

161151
assert repo.git("log", "-p", trim_newline=False).decode() == dedent(
162152
"""\
163-
commit 44fdce0cf7ae75ed5edac5f3defed83cddf3ec4a
153+
commit 31aa1057aca9f039e997fff9396fb9656fb4c01c
164154
Author: Bash Author <[email protected]>
165155
Date: Thu Jul 13 21:40:00 2017 -0500
166156
167157
add eggs
168158
169159
diff --git a/file b/file
170-
index 5d0f8a8..cb90548 100644
160+
index 5d0f8a8..2481b83 100644
171161
--- a/file
172162
+++ b/file
173163
@@ -1 +1 @@
174164
-intermediate state
175-
+something completely different
165+
+eggs spam
176166
177167
commit 1fa5135a6cce1f63dc2f5584ee68e15a4de3a99c
178168
Author: Bash Author <[email protected]>

0 commit comments

Comments
 (0)