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 | 1530 | default = false |
|
1531 | 1531 | |
|
1532 | 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 | 1551 | section = "merge-tools" |
|
1534 | 1552 | name = ".*" |
|
1535 | 1553 | generic = true |
@@ -255,6 +255,10 def debugbuilddag( | |||
|
255 | 255 | progress = ui.makeprogress( |
|
256 | 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 | 262 | with progress, repo.wlock(), repo.lock(), repo.transaction(b"builddag"): |
|
259 | 263 | at = -1 |
|
260 | 264 | atbranch = b'default' |
@@ -279,7 +283,12 def debugbuilddag( | |||
|
279 | 283 | base, local, other = [ |
|
280 | 284 | x[fn].data() for x in (pa, p1, p2) |
|
281 | 285 | ] |
|
282 |
m3 = simplemerge.Merge3Text( |
|
|
286 | m3 = simplemerge.Merge3Text( | |
|
287 | base, | |
|
288 | local, | |
|
289 | other, | |
|
290 | relaxed_sync=merge_relaxed_sync, | |
|
291 | ) | |
|
283 | 292 | ml = [ |
|
284 | 293 | l.strip() |
|
285 | 294 | for l in simplemerge.render_minimized(m3)[0] |
@@ -481,6 +481,8 def _merge(repo, local, other, base, mod | |||
|
481 | 481 | suppresses the markers.""" |
|
482 | 482 | ui = repo.ui |
|
483 | 483 | |
|
484 | relaxed_sync = ui.configbool(b'experimental', b'relaxed-block-sync-merge') | |
|
485 | ||
|
484 | 486 | try: |
|
485 | 487 | _verifytext(local, ui) |
|
486 | 488 | _verifytext(base, ui) |
@@ -489,7 +491,11 def _merge(repo, local, other, base, mod | |||
|
489 | 491 | return True, True, False |
|
490 | 492 | else: |
|
491 | 493 | merged_text, conflicts = simplemerge.simplemerge( |
|
492 |
local, |
|
|
494 | local, | |
|
495 | base, | |
|
496 | other, | |
|
497 | mode=mode, | |
|
498 | relaxed_sync=relaxed_sync, | |
|
493 | 499 | ) |
|
494 | 500 | # fcd.flags() already has the merged flags (done in |
|
495 | 501 | # mergestate.resolve()) |
@@ -49,6 +49,30 def intersect(ra, rb): | |||
|
49 | 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 | 76 | def compare_range(a, astart, aend, b, bstart, bend): |
|
53 | 77 | """Compare a[astart:aend] == b[bstart:bend], without slicing.""" |
|
54 | 78 | if (aend - astart) != (bend - bstart): |
@@ -66,7 +90,16 class Merge3Text: | |||
|
66 | 90 | Given strings BASE, OTHER, THIS, tries to produce a combined text |
|
67 | 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 | 103 | self.basetext = basetext |
|
71 | 104 | self.atext = atext |
|
72 | 105 | self.btext = btext |
@@ -76,6 +109,7 class Merge3Text: | |||
|
76 | 109 | a = mdiff.splitnewlines(atext) |
|
77 | 110 | if b is None: |
|
78 | 111 | b = mdiff.splitnewlines(btext) |
|
112 | self.relaxed_sync = relaxed_sync | |
|
79 | 113 | self.base = base |
|
80 | 114 | self.a = a |
|
81 | 115 | self.b = b |
@@ -220,6 +254,11 class Merge3Text: | |||
|
220 | 254 | len_a = len(amatches) |
|
221 | 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 | 262 | sl = [] |
|
224 | 263 | |
|
225 | 264 | while ia < len_a and ib < len_b: |
@@ -228,7 +267,7 class Merge3Text: | |||
|
228 | 267 | |
|
229 | 268 | # there is an unconflicted block at i; how long does it |
|
230 | 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 | 271 | if i: |
|
233 | 272 | intbase = i[0] |
|
234 | 273 | intend = i[1] |
@@ -258,13 +297,41 class Merge3Text: | |||
|
258 | 297 | # advance whichever one ends first in the base text |
|
259 | 298 | if (abase + alen) < (bbase + blen): |
|
260 | 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 | 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 | 327 | intbase = len(self.base) |
|
265 | 328 | abase = len(self.a) |
|
266 | 329 | bbase = len(self.b) |
|
267 |
s |
|
|
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 | 336 | return sl |
|
270 | 337 | |
@@ -498,6 +565,7 def simplemerge( | |||
|
498 | 565 | other, |
|
499 | 566 | mode=b'merge', |
|
500 | 567 | allow_binary=False, |
|
568 | relaxed_sync=False, | |
|
501 | 569 | ): |
|
502 | 570 | """Performs the simplemerge algorithm. |
|
503 | 571 | |
@@ -509,7 +577,9 def simplemerge( | |||
|
509 | 577 | _verifytext(base) |
|
510 | 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 | 583 | conflicts = False |
|
514 | 584 | if mode == b'union': |
|
515 | 585 | lines = _resolve(m3, (1, 2)) |
General Comments 0
You need to be logged in to leave comments.
Login now