Skip to content

Commit 1859fd4

Browse files
committed
Commit reordering: Avoid asking about the same conflict twice
Implements #132
1 parent f68caaf commit 1859fd4

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
@@ -36,12 +36,10 @@ def test_reuse_recorded_resolution(
3636
history_with_two_conflicting_commits(auto_update=auto_update)
3737

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

4644
tree_after_resolving_conflicts = repo.get_commit("HEAD").tree()
4745
bash("git reset --hard HEAD@{1}")
@@ -50,18 +48,16 @@ def test_reuse_recorded_resolution(
5048
acceptance_input = None
5149
intermediate_state = "spam"
5250
if not auto_update:
53-
acceptance_input = b"y\n" * 2
51+
acceptance_input = b"y\n"
5452
if custom_resolution is not None:
55-
acceptance_input = b"n\n" + b"y\n" * 4
53+
acceptance_input = b"n\n" + b"y\n" * 2
5654
intermediate_state = custom_resolution
5755

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

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

@@ -95,12 +91,10 @@ def test_rerere_merge(repo: Repository) -> None:
9591
bash("git commit -am 'commit 2'")
9692

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

@@ -123,15 +117,9 @@ def test_rerere_merge(repo: Repository) -> None:
123117
"""\
124118
@@ -1 +1 @@
125119
-resolved1
126-
+resolved2"""
127-
)
128-
leftover_index = hunks(repo.git("diff", "-U0", "HEAD"))
129-
assert leftover_index == dedent(
130-
"""\
131-
@@ -1 +1 @@
132-
-resolved2
133-
+original2"""
120+
+original2"""
134121
)
122+
assert leftover_index(repo.git("diff", "-U0", "HEAD")) == ""
135123

136124

137125
def test_replay_resolution_recorded_by_git(repo: Repository) -> None:
@@ -151,25 +139,28 @@ def test_replay_resolution_recorded_by_git(repo: Repository) -> None:
151139
"""
152140
)
153141

154-
# Now let's try to do the same thing with git-revise, reusing the recorded resolution.
142+
# Git-revise may well reuse an intermediate state resolution,
143+
# but actually knows that nobody expects the final state to change
144+
# when just changing the order it's done: Whenever the final state
145+
# is known, that pointless second conflict is bypassed.
155146
with editor_main(("-i", "HEAD~~")) as ed:
156147
flip_last_two_commits(repo, ed)
157148

158149
assert repo.git("log", "-p", trim_newline=False).decode() == dedent(
159150
"""\
160-
commit 44fdce0cf7ae75ed5edac5f3defed83cddf3ec4a
151+
commit 31aa1057aca9f039e997fff9396fb9656fb4c01c
161152
Author: Bash Author <[email protected]>
162153
Date: Thu Jul 13 21:40:00 2017 -0500
163154
164155
add eggs
165156
166157
diff --git a/file b/file
167-
index 5d0f8a8..cb90548 100644
158+
index 5d0f8a8..2481b83 100644
168159
--- a/file
169160
+++ b/file
170161
@@ -1 +1 @@
171162
-intermediate state
172-
+something completely different
163+
+eggs spam
173164
174165
commit 1fa5135a6cce1f63dc2f5584ee68e15a4de3a99c
175166
Author: Bash Author <[email protected]>

0 commit comments

Comments
 (0)