Skip to content

Commit c467c1f

Browse files
committed
Commit reordering: Avoid asking about the same conflict twice
Implements #132
1 parent dd076af commit c467c1f

File tree

5 files changed

+48
-38
lines changed

5 files changed

+48
-38
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+
known_state: Optional[Commit],
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 known_state is not None:
51+
tree = get_tree(known_state)
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -616,12 +616,12 @@ def summary(self) -> str:
616616
)
617617
return " ".join(summary_paragraph.splitlines())
618618

619-
def rebase(self, parent: Optional[Commit]) -> Commit:
619+
def rebase(self, parent: Optional[Commit], known_state: Optional[Commit]) -> Commit:
620620
"""Create a new commit with the same changes, except with ``parent``
621621
as its parent. If ``parent`` is ``None``, this becomes a root commit."""
622622
from .merge import rebase # pylint: disable=import-outside-toplevel
623623

624-
return rebase(self, parent)
624+
return rebase(self, parent, known_state)
625625

626626
def update(
627627
self,

gitrevise/todo.py

Lines changed: 14 additions & 2 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+
original: List[Step],
247248
todos: 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, step in zip(original, todos):
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.commit.oid)
258+
applied_new_commits.add(step.commit.oid)
259+
is_known = applied_new_commits == applied_old_commits
260+
known_state = known.commit if is_known else None
261+
262+
rebased = step.commit.rebase(current, known_state).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: 4 additions & 4 deletions
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)
@@ -183,8 +183,8 @@ def noninteractive(
183183
final = head.target.tree()
184184
if staged:
185185
print(f"Applying staged changes to '{args.target}'")
186-
current = current.update(tree=staged.rebase(current).tree())
187-
final = staged.rebase(head.target).tree()
186+
current = current.update(tree=staged.rebase(current, known_state=None).tree())
187+
final = staged.rebase(head.target, known_state=None).tree()
188188

189189
# Update the commit message on the target commit if requested.
190190
if args.message:
@@ -217,7 +217,7 @@ def noninteractive(
217217
for commit in to_rebase:
218218
if repo.sign_commits != bool(commit.gpgsig):
219219
commit = commit.update(recommit=True)
220-
current = commit.rebase(current)
220+
current = commit.rebase(current, known_state=None)
221221
print(f"{current.oid.short()} {current.summary()}")
222222

223223
update_head(head, current, final)

tests/test_rerere.py

Lines changed: 13 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 leftover_index(repo.git("diff", "-U0", "HEAD")) == ""
136124

137125

138126
def test_replay_resolution_recorded_by_git(repo: Repository) -> None:
@@ -154,25 +142,28 @@ 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+
# Git-revise may well reuse an intermediate state resolution,
146+
# but actually knows that nobody expects the final state to change
147+
# when just changing the order it's done: Whenever the final state
148+
# is known, that pointless second conflict is bypassed.
158149
with editor_main(("-i", "HEAD~~")) as ed:
159150
flip_last_two_commits(repo, ed)
160151

161152
assert repo.git("log", "-p", trim_newline=False).decode() == dedent(
162153
"""\
163-
commit 44fdce0cf7ae75ed5edac5f3defed83cddf3ec4a
154+
commit 31aa1057aca9f039e997fff9396fb9656fb4c01c
164155
Author: Bash Author <[email protected]>
165156
Date: Thu Jul 13 21:40:00 2017 -0500
166157
167158
add eggs
168159
169160
diff --git a/file b/file
170-
index 5d0f8a8..cb90548 100644
161+
index 5d0f8a8..2481b83 100644
171162
--- a/file
172163
+++ b/file
173164
@@ -1 +1 @@
174165
-intermediate state
175-
+something completely different
166+
+eggs spam
176167
177168
commit 1fa5135a6cce1f63dc2f5584ee68e15a4de3a99c
178169
Author: Bash Author <[email protected]>

0 commit comments

Comments
 (0)