Skip to content

Commit 8a942dd

Browse files
committed
Commit reordering: Avoid asking about the same conflict twice
Implements #132
1 parent 1f54d7c commit 8a942dd

5 files changed

Lines changed: 48 additions & 38 deletions

File tree

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:
@@ -152,25 +140,28 @@ def test_replay_resolution_recorded_by_git(repo: Repository) -> None:
152140
"""
153141
)
154142

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

159150
assert repo.git("log", "-p", trim_newline=False).decode() == dedent(
160151
"""\
161-
commit 44fdce0cf7ae75ed5edac5f3defed83cddf3ec4a
152+
commit 31aa1057aca9f039e997fff9396fb9656fb4c01c
162153
Author: Bash Author <bash_author@example.com>
163154
Date: Thu Jul 13 21:40:00 2017 -0500
164155
165156
add eggs
166157
167158
diff --git a/file b/file
168-
index 5d0f8a8..cb90548 100644
159+
index 5d0f8a8..2481b83 100644
169160
--- a/file
170161
+++ b/file
171162
@@ -1 +1 @@
172163
-intermediate state
173-
+something completely different
164+
+eggs spam
174165
175166
commit 1fa5135a6cce1f63dc2f5584ee68e15a4de3a99c
176167
Author: Bash Author <bash_author@example.com>

0 commit comments

Comments
 (0)