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( |
|
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, |
|
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 |
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 | 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