##// END OF EJS Templates
merge: add a config to allow conflict-free merge of changes on adjacent lines...
Arseniy Alekseyev -
r52974:a021da4e default
parent child Browse files
Show More
@@ -0,0 +1,154
1 ==============================================
2 Test merge algorithm with "relaxed block sync"
3 ==============================================
4
5 Setup
6 =====
7
8 $ cat >> $HGRCPATH << EOF
9 > [experimental]
10 > relaxed-block-sync-merge=yes
11 > [ui]
12 > merge=:merge3
13 > EOF
14 $ unset HGMERGE
15
16 $ hg init repo
17 $ cd repo
18
19 $ m=../scratch
20 $ mkdir "$m"
21
22 # For the purpose of this test, we use a file [listing] that has one line
23 # per file of [scratch] directory.
24 # This way, the patches can be represented as bash scripts.
25 #
26 # Adding a line is then just "touch", removing a line is "rm", and
27 # modifying a line is "echo modfied > file1".
28
29 # Make_change takes a "patch script", as described above, and
30 # produces a file [listing] with the coresponding contents
31 # past applying the patch to a fixed base state.
32 $ make_change() {
33 > cmd=$1
34 > rm -r ../scratch
35 > mkdir ../scratch
36 > (cat listing 2>/dev/null || true) | while IFS=' :' read k v; do echo "$v" > ../scratch/"$k"; done
37 >
38 > (
39 > cd ../scratch
40 > eval "$cmd" >&2
41 > for f in *; do val=$(cat "$f"); printf "$f: $val\n"; done) > listing
42 > }
43
44 # mk_rev takes a [base] and a patch, and produces a child revision of [base]
45 # corresponding to that patch.
46 $ mk_rev() {
47 > base=$1
48 > cmd=$2
49 > (hg update -C "$base" -q
50 > make_change "$cmd"
51 > (hg commit -qAm _ 2>&1) | grep -v 'commit already existed') >&2
52 > hg log -r . -T '{rev}'
53 > }
54
55 $ test() {
56 > cmd1=$1
57 > cmd2=$2
58 > r2=$(mk_rev 0 "$cmd2")
59 > r1=$(mk_rev 0 "$cmd1")
60 > # already at r1
61 > hg merge -q "$r2"
62 > cat listing
63 > }
64
65 $ rev0=$(mk_rev 'rev(-1)' 'echo val1 > key1; echo val2 > key2; echo val3 > key3; ')
66 $ cat listing
67 key1: val1
68 key2: val2
69 key3: val3
70
71 Actual testing
72 ==============
73
74 easy merge: no need for relaxed block sync:
75 -------------------------------------------
76
77 $ test 'echo modified1 > key1' 'echo modified3 > key3'
78 key1: modified1
79 key2: val2
80 key3: modified3
81
82 Add adjacent to modify:
83 -----------------------
84
85 $ test 'echo modified > key3' 'echo val4 > key4'
86 key1: val1
87 key2: val2
88 key3: modified
89 key4: val4
90
91 Modify adjacent to modify:
92 --------------------------
93
94 $ test 'echo modified3 > key3' 'echo modified2 > key2'
95 key1: val1
96 key2: modified2
97 key3: modified3
98
99 Remove adjacent to modify:
100 --------------------------
101
102 $ test 'rm key2' 'echo modified > key1'
103 key1: modified
104 key3: val3
105
106 Add adjacent to remove:
107 -----------------------
108
109 $ test 'rm key2' 'touch key1a'
110 key1: val1
111 key1a:
112 key3: val3
113
114 Remove adjacent to remove:
115 --------------------------
116
117 $ test 'rm key2' 'rm key1'
118 key3: val3
119
120 It even works if you're sandwiched between additions above and below:
121
122 $ test 'echo val-changed-3 > key3' 'touch key2a; touch key4'
123 key1: val1
124 key2: val2
125 key2a:
126 key3: val-changed-3
127 key4:
128
129 Add adjacent to add:
130 --------------------
131
132 Add adjacent to add is still disallowed because we don't know what order to add
133 lines in:
134
135 $ test 'touch key1a' 'touch key1b'
136 warning: conflicts while merging listing! (edit, then use 'hg resolve --mark')
137 key1: val1
138 <<<<<<< working copy: 744662bcc33a - test: _
139 key1a:
140 ||||||| common ancestor: b1791e356cd4 - test: _
141 =======
142 key1b:
143 >>>>>>> merge rev: 06735b47f956 - test: _
144 key2: val2
145 key3: val3
146
147 Add kinda-adjacent to add can still work if there's an
148 adjacent line that helps resolve the order ambiguity:
149
150 $ test 'touch key1a; rm key2' 'touch key2a'
151 key1: val1
152 key1a:
153 key2a:
154 key3: val3
@@ -1530,6 +1530,24 name = "strict-capability-check"
1530 default = false
1530 default = false
1531
1531
1532 [[items]]
1532 [[items]]
1533 section = "experimental"
1534 name = "relaxed-block-sync-merge"
1535 default = false
1536 documentation="""When using built-in simple merge tools, this config makes it so that changes
1537 touching adjacent file regions no longer conflict with each other.
1538
1539 In particular, addition/modification/removal adjacent to modification/removal
1540 are all allowed with no conflict.
1541
1542 Addition next to addition is still treated as a conflict because it presents
1543 a legitimate ambiguity.
1544
1545 The change tweaks existing logic for aligning file changes, making it so
1546 that a 0-length spacing between regions is just as good as a 1-line spacing.
1547 (default: False)
1548 """
1549
1550 [[items]]
1533 section = "merge-tools"
1551 section = "merge-tools"
1534 name = ".*"
1552 name = ".*"
1535 generic = true
1553 generic = true
@@ -255,6 +255,10 def debugbuilddag(
255 progress = ui.makeprogress(
255 progress = ui.makeprogress(
256 _(b'building'), unit=_(b'revisions'), total=total
256 _(b'building'), unit=_(b'revisions'), total=total
257 )
257 )
258 merge_relaxed_sync = ui.configbool(
259 b'experimental',
260 b'relaxed-block-sync-merge',
261 )
258 with progress, repo.wlock(), repo.lock(), repo.transaction(b"builddag"):
262 with progress, repo.wlock(), repo.lock(), repo.transaction(b"builddag"):
259 at = -1
263 at = -1
260 atbranch = b'default'
264 atbranch = b'default'
@@ -279,7 +283,12 def debugbuilddag(
279 base, local, other = [
283 base, local, other = [
280 x[fn].data() for x in (pa, p1, p2)
284 x[fn].data() for x in (pa, p1, p2)
281 ]
285 ]
282 m3 = simplemerge.Merge3Text(base, local, other)
286 m3 = simplemerge.Merge3Text(
287 base,
288 local,
289 other,
290 relaxed_sync=merge_relaxed_sync,
291 )
283 ml = [
292 ml = [
284 l.strip()
293 l.strip()
285 for l in simplemerge.render_minimized(m3)[0]
294 for l in simplemerge.render_minimized(m3)[0]
@@ -481,6 +481,8 def _merge(repo, local, other, base, mod
481 suppresses the markers."""
481 suppresses the markers."""
482 ui = repo.ui
482 ui = repo.ui
483
483
484 relaxed_sync = ui.configbool(b'experimental', b'relaxed-block-sync-merge')
485
484 try:
486 try:
485 _verifytext(local, ui)
487 _verifytext(local, ui)
486 _verifytext(base, ui)
488 _verifytext(base, ui)
@@ -489,7 +491,11 def _merge(repo, local, other, base, mod
489 return True, True, False
491 return True, True, False
490 else:
492 else:
491 merged_text, conflicts = simplemerge.simplemerge(
493 merged_text, conflicts = simplemerge.simplemerge(
492 local, base, other, mode=mode
494 local,
495 base,
496 other,
497 mode=mode,
498 relaxed_sync=relaxed_sync,
493 )
499 )
494 # fcd.flags() already has the merged flags (done in
500 # fcd.flags() already has the merged flags (done in
495 # mergestate.resolve())
501 # mergestate.resolve())
@@ -49,6 +49,30 def intersect(ra, rb):
49 return None
49 return None
50
50
51
51
52 def intersect_or_touch(ra, rb):
53 """Given two ranges return the range where they intersect or touch or None.
54
55 >>> intersect_or_touch((0, 10), (0, 6))
56 (0, 6)
57 >>> intersect_or_touch((0, 10), (5, 15))
58 (5, 10)
59 >>> intersect_or_touch((0, 10), (10, 15))
60 (10, 10)
61 >>> intersect_or_touch((0, 9), (10, 15))
62 >>> intersect_or_touch((0, 9), (7, 15))
63 (7, 9)
64 """
65 assert ra[0] <= ra[1]
66 assert rb[0] <= rb[1]
67
68 sa = max(ra[0], rb[0])
69 sb = min(ra[1], rb[1])
70 if sa <= sb:
71 return sa, sb
72 else:
73 return None
74
75
52 def compare_range(a, astart, aend, b, bstart, bend):
76 def compare_range(a, astart, aend, b, bstart, bend):
53 """Compare a[astart:aend] == b[bstart:bend], without slicing."""
77 """Compare a[astart:aend] == b[bstart:bend], without slicing."""
54 if (aend - astart) != (bend - bstart):
78 if (aend - astart) != (bend - bstart):
@@ -66,7 +90,16 class Merge3Text:
66 Given strings BASE, OTHER, THIS, tries to produce a combined text
90 Given strings BASE, OTHER, THIS, tries to produce a combined text
67 incorporating the changes from both BASE->OTHER and BASE->THIS."""
91 incorporating the changes from both BASE->OTHER and BASE->THIS."""
68
92
69 def __init__(self, basetext, atext, btext, base=None, a=None, b=None):
93 def __init__(
94 self,
95 basetext,
96 atext,
97 btext,
98 base=None,
99 a=None,
100 b=None,
101 relaxed_sync=False,
102 ):
70 self.basetext = basetext
103 self.basetext = basetext
71 self.atext = atext
104 self.atext = atext
72 self.btext = btext
105 self.btext = btext
@@ -76,6 +109,7 class Merge3Text:
76 a = mdiff.splitnewlines(atext)
109 a = mdiff.splitnewlines(atext)
77 if b is None:
110 if b is None:
78 b = mdiff.splitnewlines(btext)
111 b = mdiff.splitnewlines(btext)
112 self.relaxed_sync = relaxed_sync
79 self.base = base
113 self.base = base
80 self.a = a
114 self.a = a
81 self.b = b
115 self.b = b
@@ -220,6 +254,11 class Merge3Text:
220 len_a = len(amatches)
254 len_a = len(amatches)
221 len_b = len(bmatches)
255 len_b = len(bmatches)
222
256
257 if self.relaxed_sync:
258 intersect_fun = intersect_or_touch
259 else:
260 intersect_fun = intersect
261
223 sl = []
262 sl = []
224
263
225 while ia < len_a and ib < len_b:
264 while ia < len_a and ib < len_b:
@@ -228,7 +267,7 class Merge3Text:
228
267
229 # there is an unconflicted block at i; how long does it
268 # there is an unconflicted block at i; how long does it
230 # extend? until whichever one ends earlier.
269 # extend? until whichever one ends earlier.
231 i = intersect((abase, abase + alen), (bbase, bbase + blen))
270 i = intersect_fun((abase, abase + alen), (bbase, bbase + blen))
232 if i:
271 if i:
233 intbase = i[0]
272 intbase = i[0]
234 intend = i[1]
273 intend = i[1]
@@ -258,13 +297,41 class Merge3Text:
258 # advance whichever one ends first in the base text
297 # advance whichever one ends first in the base text
259 if (abase + alen) < (bbase + blen):
298 if (abase + alen) < (bbase + blen):
260 ia += 1
299 ia += 1
300 elif not self.relaxed_sync:
301 # if the blocks end at the same time we know they can't overlap
302 # any other block, so no need for the complicated checks below
303 ib += 1
304 elif (abase + alen) > (bbase + blen):
305 ib += 1
261 else:
306 else:
262 ib += 1
307 # If both end at the same time, either may touching the
308 # follow-up matching block on the other side.
309 # Advance the one whose next block comes sooner.
310 if ia + 1 == len_a:
311 # if we run out of blocks on A side, we may as well advance B
312 # since there's nothing on A side for that to touch
313 ib += 1
314 elif ib + 1 == len_b:
315 ia += 1
316 elif amatches[ia + 1][0] > bmatches[ib + 1][0]:
317 ib += 1
318 elif amatches[ia + 1][0] < bmatches[ib + 1][0]:
319 ia += 1
320 else:
321 # symmetric situation: both sides added lines to the same place
322 # it's less surprising if we treat it as a conflict, so skip
323 # both without a preferred order
324 ia += 1
325 ib += 1
263
326
264 intbase = len(self.base)
327 intbase = len(self.base)
265 abase = len(self.a)
328 abase = len(self.a)
266 bbase = len(self.b)
329 bbase = len(self.b)
267 sl.append((intbase, intbase, abase, abase, bbase, bbase))
330 sentinel_hunk = (intbase, intbase, abase, abase, bbase, bbase)
331 # we avoid duplicate sentinel hunk at the end to make the
332 # test output cleaner
333 if not (sl and sl[len(sl) - 1] == sentinel_hunk):
334 sl.append(sentinel_hunk)
268
335
269 return sl
336 return sl
270
337
@@ -498,6 +565,7 def simplemerge(
498 other,
565 other,
499 mode=b'merge',
566 mode=b'merge',
500 allow_binary=False,
567 allow_binary=False,
568 relaxed_sync=False,
501 ):
569 ):
502 """Performs the simplemerge algorithm.
570 """Performs the simplemerge algorithm.
503
571
@@ -509,7 +577,9 def simplemerge(
509 _verifytext(base)
577 _verifytext(base)
510 _verifytext(other)
578 _verifytext(other)
511
579
512 m3 = Merge3Text(base.text(), local.text(), other.text())
580 m3 = Merge3Text(
581 base.text(), local.text(), other.text(), relaxed_sync=relaxed_sync
582 )
513 conflicts = False
583 conflicts = False
514 if mode == b'union':
584 if mode == b'union':
515 lines = _resolve(m3, (1, 2))
585 lines = _resolve(m3, (1, 2))
General Comments 0
You need to be logged in to leave comments. Login now