Skip to content

Commit 9f50dae

Browse files
committed
Merge branch 'conflict-marker' into 'conflict-marker-size'
2 parents 0595a93 + b2e83ab commit 9f50dae

File tree

2 files changed

+71
-29
lines changed

2 files changed

+71
-29
lines changed

gitrevise/merge.py

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@
2626
T = TypeVar("T") # pylint: disable=C0103
2727

2828

29+
# See git/xdiff/xdiff.h
30+
DEFAULT_CONFLICT_MARKER_SIZE = 7
31+
32+
33+
def get_conflict_marker_size(__repo: Repository, __file: Path) -> int:
34+
# TODO: Determine on a per-file basis by its `conflict-marker-size` attribute.
35+
# See ll_merge_marker_size in git/ll-merge.c
36+
return DEFAULT_CONFLICT_MARKER_SIZE
37+
38+
2939
class MergeConflict(Exception):
3040
pass
3141

@@ -198,22 +208,26 @@ def merge_blobs(
198208
base: Optional[Blob],
199209
other: Blob,
200210
) -> Blob:
211+
# pylint: disable=too-many-locals
201212
repo = current.repo
202213

203214
tmpdir = repo.get_tempdir()
204215

216+
marker_size = get_conflict_marker_size(repo, path)
217+
205218
annotated_labels = (
206219
f"{path} (new parent): {labels[0]}",
207220
f"{path} (old parent): {labels[1]}",
208221
f"{path} (current): {labels[2]}",
209222
)
210223
(is_clean_merge, merged) = merge_files(
211224
repo,
225+
tmpdir,
226+
marker_size,
212227
annotated_labels,
213228
current.body,
214229
base.body if base else b"",
215230
other.body,
216-
tmpdir,
217231
)
218232

219233
if is_clean_merge:
@@ -227,7 +241,10 @@ def merge_blobs(
227241

228242
preimage = merged
229243
(normalized_preimage, conflict_id, merged_blob) = replay_recorded_resolution(
230-
repo, tmpdir, preimage
244+
repo=repo,
245+
tmpdir=tmpdir,
246+
marker_size=marker_size,
247+
preimage=preimage,
231248
)
232249
if merged_blob is not None:
233250
return merged_blob
@@ -246,7 +263,7 @@ def merge_blobs(
246263
if merged == preimage:
247264
print("(note) conflicted file is unchanged")
248265

249-
if b"<<<<<<<" in merged or b"=======" in merged or b">>>>>>>" in merged:
266+
if any((marker * marker_size) in merged for marker in (b"<", b"=", b">")):
250267
print("(note) conflict markers found in the merged file")
251268

252269
# Was the merge successful?
@@ -260,11 +277,12 @@ def merge_blobs(
260277

261278
def merge_files(
262279
repo: Repository,
280+
tmpdir: Path,
281+
marker_size: int,
263282
labels: Tuple[str, str, str],
264283
current: bytes,
265284
base: bytes,
266285
other: bytes,
267-
tmpdir: Path,
268286
) -> Tuple[bool, bytes]:
269287
(tmpdir / "current").write_bytes(current)
270288
(tmpdir / "base").write_bytes(base)
@@ -274,8 +292,13 @@ def merge_files(
274292
try:
275293
merged = repo.git(
276294
"merge-file",
277-
"-q",
278-
"-p",
295+
# Do not print warnings on conflicts.
296+
"--quiet",
297+
# Send results to stdout instead of overwriting "current file".
298+
"--stdout",
299+
# Ensure markers are the expected length to ensure consistent rerere results.
300+
"--marker-size",
301+
str(marker_size),
279302
f"-L{labels[0]}",
280303
f"-L{labels[1]}",
281304
f"-L{labels[2]}",
@@ -296,7 +319,10 @@ def merge_files(
296319

297320

298321
def replay_recorded_resolution(
299-
repo: Repository, tmpdir: Path, preimage: bytes
322+
repo: Repository,
323+
tmpdir: Path,
324+
marker_size: int,
325+
preimage: bytes,
300326
) -> Tuple[bytes, Optional[str], Optional[Blob]]:
301327
rr_cache = repo.git_path("rr-cache")
302328
if not repo.bool_config(
@@ -305,7 +331,11 @@ def replay_recorded_resolution(
305331
):
306332
return (b"", None, None)
307333

308-
(normalized_preimage, conflict_id) = normalize_conflicted_file(preimage)
334+
(normalized_preimage, conflict_id) = normalize_conflicted_file(
335+
marker_size=marker_size,
336+
body=preimage,
337+
)
338+
309339
conflict_dir = rr_cache / conflict_id
310340
if not conflict_dir.is_dir():
311341
return (normalized_preimage, conflict_id, None)
@@ -324,11 +354,12 @@ def replay_recorded_resolution(
324354

325355
(is_clean_merge, merged) = merge_files(
326356
repo,
357+
tmpdir=tmpdir,
358+
marker_size=marker_size,
327359
labels=("recorded postimage", "recorded preimage", "new preimage"),
328360
current=recorded_postimage,
329361
base=recorded_preimage,
330362
other=normalized_preimage,
331-
tmpdir=tmpdir,
332363
)
333364
if not is_clean_merge:
334365
# We could ask the user to merge this. However, that could be confusing.
@@ -366,6 +397,7 @@ class ConflictParseFailed(Exception):
366397

367398

368399
def normalize_conflict(
400+
marker_size: int,
369401
lines: Iterator[bytes],
370402
hasher: Optional[hashlib._Hash],
371403
) -> bytes:
@@ -375,25 +407,25 @@ def normalize_conflict(
375407
line = next(lines, None)
376408
if line is None:
377409
raise ConflictParseFailed("unexpected eof")
378-
if line.startswith(b"<<<<<<<"):
410+
if line.startswith(b"<" * marker_size):
379411
# parse recursive conflicts, including their processed output in the current hunk
380-
conflict = normalize_conflict(lines, None)
412+
conflict = normalize_conflict(marker_size, lines, None)
381413
if cur_hunk is not None:
382414
cur_hunk += conflict
383-
elif line.startswith(b"|||||||"):
415+
elif line.startswith(b"|" * marker_size):
384416
# ignore the diff3 original section. Must be still parsing the first hunk.
385417
if other_hunk is not None:
386418
raise ConflictParseFailed("unexpected ||||||| conflict marker")
387419
(other_hunk, cur_hunk) = (cur_hunk, None)
388-
elif line.startswith(b"======="):
420+
elif line.startswith(b"=" * marker_size):
389421
# switch into the second hunk
390422
# could be in either the diff3 original section or the first hunk
391423
if cur_hunk is not None:
392424
if other_hunk is not None:
393425
raise ConflictParseFailed("unexpected ======= conflict marker")
394426
other_hunk = cur_hunk
395427
cur_hunk = b""
396-
elif line.startswith(b">>>>>>>"):
428+
elif line.startswith(b">" * marker_size):
397429
# end of conflict. update hasher, and return a normalized conflict
398430
if cur_hunk is None or other_hunk is None:
399431
raise ConflictParseFailed("unexpected >>>>>>> conflict marker")
@@ -404,11 +436,14 @@ def normalize_conflict(
404436
hasher.update(hunk2 + b"\0")
405437
return b"".join(
406438
(
407-
b"<<<<<<<\n",
439+
b"<" * marker_size,
440+
b"\n",
408441
hunk1,
409-
b"=======\n",
442+
b"=" * marker_size,
443+
b"\n",
410444
hunk2,
411-
b">>>>>>>\n",
445+
b">" * marker_size,
446+
b"\n",
412447
)
413448
)
414449
elif cur_hunk is not None:
@@ -417,7 +452,10 @@ def normalize_conflict(
417452
cur_hunk += line
418453

419454

420-
def normalize_conflicted_file(body: bytes) -> Tuple[bytes, str]:
455+
def normalize_conflicted_file(
456+
marker_size: int,
457+
body: bytes,
458+
) -> Tuple[bytes, str]:
421459
hasher = hashlib.sha1()
422460
normalized = b""
423461

@@ -426,7 +464,7 @@ def normalize_conflicted_file(body: bytes) -> Tuple[bytes, str]:
426464
line = next(lines, None)
427465
if line is None:
428466
return (normalized, hasher.hexdigest())
429-
if line.startswith(b"<<<<<<<"):
430-
normalized += normalize_conflict(lines, hasher)
467+
if line.startswith(b"<" * marker_size):
468+
normalized += normalize_conflict(marker_size, lines, hasher)
431469
else:
432470
normalized += line

tests/test_rerere.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import textwrap
44

55
from conftest import *
6-
from gitrevise.merge import normalize_conflicted_file
6+
from gitrevise.merge import DEFAULT_CONFLICT_MARKER_SIZE, normalize_conflicted_file
77

88

99
def history_with_two_conflicting_commits(autoUpdate: bool = False):
@@ -221,7 +221,8 @@ def test_replay_resolution_recorded_by_git(repo):
221221
def test_normalize_conflicted_file():
222222
# Normalize conflict markers and labels.
223223
assert normalize_conflicted_file(
224-
dedent(
224+
marker_size=DEFAULT_CONFLICT_MARKER_SIZE,
225+
body=dedent(
225226
"""\
226227
<<<<<<< HEAD
227228
a
@@ -237,7 +238,7 @@ def test_normalize_conflicted_file():
237238
d
238239
>>>>>>>>>> longer conflict marker, to be trimmed
239240
"""
240-
)
241+
),
241242
) == (
242243
dedent(
243244
"""\
@@ -262,7 +263,8 @@ def test_normalize_conflicted_file():
262263
# Discard original-text-marker from merge.conflictStyle diff3.
263264
assert (
264265
normalize_conflicted_file(
265-
dedent(
266+
marker_size=DEFAULT_CONFLICT_MARKER_SIZE,
267+
body=dedent(
266268
"""\
267269
<<<<<<< theirs
268270
a
@@ -272,7 +274,7 @@ def test_normalize_conflicted_file():
272274
c
273275
>>>>>>> ours
274276
"""
275-
)
277+
),
276278
)[0]
277279
== dedent(
278280
"""\
@@ -288,15 +290,16 @@ def test_normalize_conflicted_file():
288290
# The two sides of the conflict are ordered.
289291
assert (
290292
normalize_conflicted_file(
291-
dedent(
293+
marker_size=DEFAULT_CONFLICT_MARKER_SIZE,
294+
body=dedent(
292295
"""\
293296
<<<<<<< this way round
294297
b
295298
=======
296299
a
297300
>>>>>>> (unsorted)
298301
"""
299-
)
302+
),
300303
)[0]
301304
== dedent(
302305
"""\
@@ -312,7 +315,8 @@ def test_normalize_conflicted_file():
312315
# Nested conflict markers.
313316
assert (
314317
normalize_conflicted_file(
315-
dedent(
318+
marker_size=DEFAULT_CONFLICT_MARKER_SIZE,
319+
body=dedent(
316320
"""\
317321
<<<<<<<
318322
outer left
@@ -327,7 +331,7 @@ def test_normalize_conflicted_file():
327331
outer right
328332
>>>>>>>
329333
"""
330-
)
334+
),
331335
)[0]
332336
== dedent(
333337
"""\

0 commit comments

Comments
 (0)