2626T = 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+
2939class 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
261278def 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
298321def 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
368399def 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
0 commit comments