Mercurial > hg
comparison contrib/simplemerge @ 6002:abd66eb0889e
merge: move the bulk of simplemerge into core
- keep existing simplemerge command in contrib
- clean up test interface
author | Matt Mackall <mpm@selenic.com> |
---|---|
date | Sun, 03 Feb 2008 19:29:05 -0600 |
parents | ea7b982b6c08 |
children | a6477aa893b8 |
comparison
equal
deleted
inserted
replaced
6001:30d2fecaab76 | 6002:abd66eb0889e |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (C) 2004, 2005 Canonical Ltd | |
3 # | |
4 # This program is free software; you can redistribute it and/or modify | |
5 # it under the terms of the GNU General Public License as published by | |
6 # the Free Software Foundation; either version 2 of the License, or | |
7 # (at your option) any later version. | |
8 # | |
9 # This program is distributed in the hope that it will be useful, | |
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 # GNU General Public License for more details. | |
13 # | |
14 # You should have received a copy of the GNU General Public License | |
15 # along with this program; if not, write to the Free Software | |
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
17 | |
18 | |
19 # mbp: "you know that thing where cvs gives you conflict markers?" | |
20 # s: "i hate that." | |
21 | 2 |
22 from mercurial import demandimport | 3 from mercurial import demandimport |
23 demandimport.enable() | 4 demandimport.enable() |
24 | 5 |
25 from mercurial import util, mdiff, fancyopts | 6 import os, sys |
26 from mercurial.i18n import _ | 7 from mercurial.i18n import _ |
27 | 8 from mercurial import simplemerge, fancyopts, util |
28 | |
29 class CantReprocessAndShowBase(Exception): | |
30 pass | |
31 | |
32 | |
33 def warn(message): | |
34 sys.stdout.flush() | |
35 sys.stderr.write(message) | |
36 sys.stderr.flush() | |
37 | |
38 | |
39 def intersect(ra, rb): | |
40 """Given two ranges return the range where they intersect or None. | |
41 | |
42 >>> intersect((0, 10), (0, 6)) | |
43 (0, 6) | |
44 >>> intersect((0, 10), (5, 15)) | |
45 (5, 10) | |
46 >>> intersect((0, 10), (10, 15)) | |
47 >>> intersect((0, 9), (10, 15)) | |
48 >>> intersect((0, 9), (7, 15)) | |
49 (7, 9) | |
50 """ | |
51 assert ra[0] <= ra[1] | |
52 assert rb[0] <= rb[1] | |
53 | |
54 sa = max(ra[0], rb[0]) | |
55 sb = min(ra[1], rb[1]) | |
56 if sa < sb: | |
57 return sa, sb | |
58 else: | |
59 return None | |
60 | |
61 | |
62 def compare_range(a, astart, aend, b, bstart, bend): | |
63 """Compare a[astart:aend] == b[bstart:bend], without slicing. | |
64 """ | |
65 if (aend-astart) != (bend-bstart): | |
66 return False | |
67 for ia, ib in zip(xrange(astart, aend), xrange(bstart, bend)): | |
68 if a[ia] != b[ib]: | |
69 return False | |
70 else: | |
71 return True | |
72 | |
73 | |
74 | |
75 | |
76 class Merge3Text(object): | |
77 """3-way merge of texts. | |
78 | |
79 Given strings BASE, OTHER, THIS, tries to produce a combined text | |
80 incorporating the changes from both BASE->OTHER and BASE->THIS.""" | |
81 def __init__(self, basetext, atext, btext, base=None, a=None, b=None): | |
82 self.basetext = basetext | |
83 self.atext = atext | |
84 self.btext = btext | |
85 if base is None: | |
86 base = mdiff.splitnewlines(basetext) | |
87 if a is None: | |
88 a = mdiff.splitnewlines(atext) | |
89 if b is None: | |
90 b = mdiff.splitnewlines(btext) | |
91 self.base = base | |
92 self.a = a | |
93 self.b = b | |
94 | |
95 | |
96 | |
97 def merge_lines(self, | |
98 name_a=None, | |
99 name_b=None, | |
100 name_base=None, | |
101 start_marker='<<<<<<<', | |
102 mid_marker='=======', | |
103 end_marker='>>>>>>>', | |
104 base_marker=None, | |
105 reprocess=False): | |
106 """Return merge in cvs-like form. | |
107 """ | |
108 self.conflicts = False | |
109 newline = '\n' | |
110 if len(self.a) > 0: | |
111 if self.a[0].endswith('\r\n'): | |
112 newline = '\r\n' | |
113 elif self.a[0].endswith('\r'): | |
114 newline = '\r' | |
115 if base_marker and reprocess: | |
116 raise CantReprocessAndShowBase() | |
117 if name_a: | |
118 start_marker = start_marker + ' ' + name_a | |
119 if name_b: | |
120 end_marker = end_marker + ' ' + name_b | |
121 if name_base and base_marker: | |
122 base_marker = base_marker + ' ' + name_base | |
123 merge_regions = self.merge_regions() | |
124 if reprocess is True: | |
125 merge_regions = self.reprocess_merge_regions(merge_regions) | |
126 for t in merge_regions: | |
127 what = t[0] | |
128 if what == 'unchanged': | |
129 for i in range(t[1], t[2]): | |
130 yield self.base[i] | |
131 elif what == 'a' or what == 'same': | |
132 for i in range(t[1], t[2]): | |
133 yield self.a[i] | |
134 elif what == 'b': | |
135 for i in range(t[1], t[2]): | |
136 yield self.b[i] | |
137 elif what == 'conflict': | |
138 self.conflicts = True | |
139 yield start_marker + newline | |
140 for i in range(t[3], t[4]): | |
141 yield self.a[i] | |
142 if base_marker is not None: | |
143 yield base_marker + newline | |
144 for i in range(t[1], t[2]): | |
145 yield self.base[i] | |
146 yield mid_marker + newline | |
147 for i in range(t[5], t[6]): | |
148 yield self.b[i] | |
149 yield end_marker + newline | |
150 else: | |
151 raise ValueError(what) | |
152 | |
153 | |
154 | |
155 | |
156 | |
157 def merge_annotated(self): | |
158 """Return merge with conflicts, showing origin of lines. | |
159 | |
160 Most useful for debugging merge. | |
161 """ | |
162 for t in self.merge_regions(): | |
163 what = t[0] | |
164 if what == 'unchanged': | |
165 for i in range(t[1], t[2]): | |
166 yield 'u | ' + self.base[i] | |
167 elif what == 'a' or what == 'same': | |
168 for i in range(t[1], t[2]): | |
169 yield what[0] + ' | ' + self.a[i] | |
170 elif what == 'b': | |
171 for i in range(t[1], t[2]): | |
172 yield 'b | ' + self.b[i] | |
173 elif what == 'conflict': | |
174 yield '<<<<\n' | |
175 for i in range(t[3], t[4]): | |
176 yield 'A | ' + self.a[i] | |
177 yield '----\n' | |
178 for i in range(t[5], t[6]): | |
179 yield 'B | ' + self.b[i] | |
180 yield '>>>>\n' | |
181 else: | |
182 raise ValueError(what) | |
183 | |
184 | |
185 | |
186 | |
187 | |
188 def merge_groups(self): | |
189 """Yield sequence of line groups. Each one is a tuple: | |
190 | |
191 'unchanged', lines | |
192 Lines unchanged from base | |
193 | |
194 'a', lines | |
195 Lines taken from a | |
196 | |
197 'same', lines | |
198 Lines taken from a (and equal to b) | |
199 | |
200 'b', lines | |
201 Lines taken from b | |
202 | |
203 'conflict', base_lines, a_lines, b_lines | |
204 Lines from base were changed to either a or b and conflict. | |
205 """ | |
206 for t in self.merge_regions(): | |
207 what = t[0] | |
208 if what == 'unchanged': | |
209 yield what, self.base[t[1]:t[2]] | |
210 elif what == 'a' or what == 'same': | |
211 yield what, self.a[t[1]:t[2]] | |
212 elif what == 'b': | |
213 yield what, self.b[t[1]:t[2]] | |
214 elif what == 'conflict': | |
215 yield (what, | |
216 self.base[t[1]:t[2]], | |
217 self.a[t[3]:t[4]], | |
218 self.b[t[5]:t[6]]) | |
219 else: | |
220 raise ValueError(what) | |
221 | |
222 | |
223 def merge_regions(self): | |
224 """Return sequences of matching and conflicting regions. | |
225 | |
226 This returns tuples, where the first value says what kind we | |
227 have: | |
228 | |
229 'unchanged', start, end | |
230 Take a region of base[start:end] | |
231 | |
232 'same', astart, aend | |
233 b and a are different from base but give the same result | |
234 | |
235 'a', start, end | |
236 Non-clashing insertion from a[start:end] | |
237 | |
238 Method is as follows: | |
239 | |
240 The two sequences align only on regions which match the base | |
241 and both descendents. These are found by doing a two-way diff | |
242 of each one against the base, and then finding the | |
243 intersections between those regions. These "sync regions" | |
244 are by definition unchanged in both and easily dealt with. | |
245 | |
246 The regions in between can be in any of three cases: | |
247 conflicted, or changed on only one side. | |
248 """ | |
249 | |
250 # section a[0:ia] has been disposed of, etc | |
251 iz = ia = ib = 0 | |
252 | |
253 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): | |
254 #print 'match base [%d:%d]' % (zmatch, zend) | |
255 | |
256 matchlen = zend - zmatch | |
257 assert matchlen >= 0 | |
258 assert matchlen == (aend - amatch) | |
259 assert matchlen == (bend - bmatch) | |
260 | |
261 len_a = amatch - ia | |
262 len_b = bmatch - ib | |
263 len_base = zmatch - iz | |
264 assert len_a >= 0 | |
265 assert len_b >= 0 | |
266 assert len_base >= 0 | |
267 | |
268 #print 'unmatched a=%d, b=%d' % (len_a, len_b) | |
269 | |
270 if len_a or len_b: | |
271 # try to avoid actually slicing the lists | |
272 equal_a = compare_range(self.a, ia, amatch, | |
273 self.base, iz, zmatch) | |
274 equal_b = compare_range(self.b, ib, bmatch, | |
275 self.base, iz, zmatch) | |
276 same = compare_range(self.a, ia, amatch, | |
277 self.b, ib, bmatch) | |
278 | |
279 if same: | |
280 yield 'same', ia, amatch | |
281 elif equal_a and not equal_b: | |
282 yield 'b', ib, bmatch | |
283 elif equal_b and not equal_a: | |
284 yield 'a', ia, amatch | |
285 elif not equal_a and not equal_b: | |
286 yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch | |
287 else: | |
288 raise AssertionError("can't handle a=b=base but unmatched") | |
289 | |
290 ia = amatch | |
291 ib = bmatch | |
292 iz = zmatch | |
293 | |
294 # if the same part of the base was deleted on both sides | |
295 # that's OK, we can just skip it. | |
296 | |
297 | |
298 if matchlen > 0: | |
299 assert ia == amatch | |
300 assert ib == bmatch | |
301 assert iz == zmatch | |
302 | |
303 yield 'unchanged', zmatch, zend | |
304 iz = zend | |
305 ia = aend | |
306 ib = bend | |
307 | |
308 | |
309 def reprocess_merge_regions(self, merge_regions): | |
310 """Where there are conflict regions, remove the agreed lines. | |
311 | |
312 Lines where both A and B have made the same changes are | |
313 eliminated. | |
314 """ | |
315 for region in merge_regions: | |
316 if region[0] != "conflict": | |
317 yield region | |
318 continue | |
319 type, iz, zmatch, ia, amatch, ib, bmatch = region | |
320 a_region = self.a[ia:amatch] | |
321 b_region = self.b[ib:bmatch] | |
322 matches = mdiff.get_matching_blocks(''.join(a_region), | |
323 ''.join(b_region)) | |
324 next_a = ia | |
325 next_b = ib | |
326 for region_ia, region_ib, region_len in matches[:-1]: | |
327 region_ia += ia | |
328 region_ib += ib | |
329 reg = self.mismatch_region(next_a, region_ia, next_b, | |
330 region_ib) | |
331 if reg is not None: | |
332 yield reg | |
333 yield 'same', region_ia, region_len+region_ia | |
334 next_a = region_ia + region_len | |
335 next_b = region_ib + region_len | |
336 reg = self.mismatch_region(next_a, amatch, next_b, bmatch) | |
337 if reg is not None: | |
338 yield reg | |
339 | |
340 | |
341 def mismatch_region(next_a, region_ia, next_b, region_ib): | |
342 if next_a < region_ia or next_b < region_ib: | |
343 return 'conflict', None, None, next_a, region_ia, next_b, region_ib | |
344 mismatch_region = staticmethod(mismatch_region) | |
345 | |
346 | |
347 def find_sync_regions(self): | |
348 """Return a list of sync regions, where both descendents match the base. | |
349 | |
350 Generates a list of (base1, base2, a1, a2, b1, b2). There is | |
351 always a zero-length sync region at the end of all the files. | |
352 """ | |
353 | |
354 ia = ib = 0 | |
355 amatches = mdiff.get_matching_blocks(self.basetext, self.atext) | |
356 bmatches = mdiff.get_matching_blocks(self.basetext, self.btext) | |
357 len_a = len(amatches) | |
358 len_b = len(bmatches) | |
359 | |
360 sl = [] | |
361 | |
362 while ia < len_a and ib < len_b: | |
363 abase, amatch, alen = amatches[ia] | |
364 bbase, bmatch, blen = bmatches[ib] | |
365 | |
366 # there is an unconflicted block at i; how long does it | |
367 # extend? until whichever one ends earlier. | |
368 i = intersect((abase, abase+alen), (bbase, bbase+blen)) | |
369 if i: | |
370 intbase = i[0] | |
371 intend = i[1] | |
372 intlen = intend - intbase | |
373 | |
374 # found a match of base[i[0], i[1]]; this may be less than | |
375 # the region that matches in either one | |
376 assert intlen <= alen | |
377 assert intlen <= blen | |
378 assert abase <= intbase | |
379 assert bbase <= intbase | |
380 | |
381 asub = amatch + (intbase - abase) | |
382 bsub = bmatch + (intbase - bbase) | |
383 aend = asub + intlen | |
384 bend = bsub + intlen | |
385 | |
386 assert self.base[intbase:intend] == self.a[asub:aend], \ | |
387 (self.base[intbase:intend], self.a[asub:aend]) | |
388 | |
389 assert self.base[intbase:intend] == self.b[bsub:bend] | |
390 | |
391 sl.append((intbase, intend, | |
392 asub, aend, | |
393 bsub, bend)) | |
394 | |
395 # advance whichever one ends first in the base text | |
396 if (abase + alen) < (bbase + blen): | |
397 ia += 1 | |
398 else: | |
399 ib += 1 | |
400 | |
401 intbase = len(self.base) | |
402 abase = len(self.a) | |
403 bbase = len(self.b) | |
404 sl.append((intbase, intbase, abase, abase, bbase, bbase)) | |
405 | |
406 return sl | |
407 | |
408 | |
409 | |
410 def find_unconflicted(self): | |
411 """Return a list of ranges in base that are not conflicted.""" | |
412 am = mdiff.get_matching_blocks(self.basetext, self.atext) | |
413 bm = mdiff.get_matching_blocks(self.basetext, self.btext) | |
414 | |
415 unc = [] | |
416 | |
417 while am and bm: | |
418 # there is an unconflicted block at i; how long does it | |
419 # extend? until whichever one ends earlier. | |
420 a1 = am[0][0] | |
421 a2 = a1 + am[0][2] | |
422 b1 = bm[0][0] | |
423 b2 = b1 + bm[0][2] | |
424 i = intersect((a1, a2), (b1, b2)) | |
425 if i: | |
426 unc.append(i) | |
427 | |
428 if a2 < b2: | |
429 del am[0] | |
430 else: | |
431 del bm[0] | |
432 | |
433 return unc | |
434 | |
435 | |
436 # bzr compatible interface, for the tests | |
437 class Merge3(Merge3Text): | |
438 """3-way merge of texts. | |
439 | |
440 Given BASE, OTHER, THIS, tries to produce a combined text | |
441 incorporating the changes from both BASE->OTHER and BASE->THIS. | |
442 All three will typically be sequences of lines.""" | |
443 def __init__(self, base, a, b): | |
444 basetext = '\n'.join([i.strip('\n') for i in base] + ['']) | |
445 atext = '\n'.join([i.strip('\n') for i in a] + ['']) | |
446 btext = '\n'.join([i.strip('\n') for i in b] + ['']) | |
447 if util.binary(basetext) or util.binary(atext) or util.binary(btext): | |
448 raise util.Abort(_("don't know how to merge binary files")) | |
449 Merge3Text.__init__(self, basetext, atext, btext, base, a, b) | |
450 | |
451 | |
452 def simplemerge(local, base, other, **opts): | |
453 def readfile(filename): | |
454 f = open(filename, "rb") | |
455 text = f.read() | |
456 f.close() | |
457 if util.binary(text): | |
458 msg = _("%s looks like a binary file.") % filename | |
459 if not opts.get('text'): | |
460 raise util.Abort(msg) | |
461 elif not opts.get('quiet'): | |
462 warn(_('warning: %s\n') % msg) | |
463 return text | |
464 | |
465 name_a = local | |
466 name_b = other | |
467 labels = opts.get('label', []) | |
468 if labels: | |
469 name_a = labels.pop(0) | |
470 if labels: | |
471 name_b = labels.pop(0) | |
472 if labels: | |
473 raise util.Abort(_("can only specify two labels.")) | |
474 | |
475 localtext = readfile(local) | |
476 basetext = readfile(base) | |
477 othertext = readfile(other) | |
478 | |
479 orig = local | |
480 local = os.path.realpath(local) | |
481 if not opts.get('print'): | |
482 opener = util.opener(os.path.dirname(local)) | |
483 out = opener(os.path.basename(local), "w", atomictemp=True) | |
484 else: | |
485 out = sys.stdout | |
486 | |
487 reprocess = not opts.get('no_minimal') | |
488 | |
489 m3 = Merge3Text(basetext, localtext, othertext) | |
490 for line in m3.merge_lines(name_a=name_a, name_b=name_b, | |
491 reprocess=reprocess): | |
492 out.write(line) | |
493 | |
494 if not opts.get('print'): | |
495 out.rename() | |
496 | |
497 if m3.conflicts: | |
498 if not opts.get('quiet'): | |
499 warn(_("warning: conflicts during merge.\n")) | |
500 return 1 | |
501 | 9 |
502 options = [('L', 'label', [], _('labels to use on conflict markers')), | 10 options = [('L', 'label', [], _('labels to use on conflict markers')), |
503 ('a', 'text', None, _('treat all files as text')), | 11 ('a', 'text', None, _('treat all files as text')), |
504 ('p', 'print', None, | 12 ('p', 'print', None, |
505 _('print results instead of overwriting LOCAL')), | 13 _('print results instead of overwriting LOCAL')), |
515 Apply to LOCAL the changes necessary to go from BASE to OTHER. | 23 Apply to LOCAL the changes necessary to go from BASE to OTHER. |
516 | 24 |
517 By default, LOCAL is overwritten with the results of this operation. | 25 By default, LOCAL is overwritten with the results of this operation. |
518 ''') | 26 ''') |
519 | 27 |
28 class ParseError(Exception): | |
29 """Exception raised on errors in parsing the command line.""" | |
30 | |
520 def showhelp(): | 31 def showhelp(): |
521 sys.stdout.write(usage) | 32 sys.stdout.write(usage) |
522 sys.stdout.write('\noptions:\n') | 33 sys.stdout.write('\noptions:\n') |
523 | 34 |
524 out_opts = [] | 35 out_opts = [] |
528 '%s' % desc)) | 39 '%s' % desc)) |
529 opts_len = max([len(opt[0]) for opt in out_opts]) | 40 opts_len = max([len(opt[0]) for opt in out_opts]) |
530 for first, second in out_opts: | 41 for first, second in out_opts: |
531 sys.stdout.write(' %-*s %s\n' % (opts_len, first, second)) | 42 sys.stdout.write(' %-*s %s\n' % (opts_len, first, second)) |
532 | 43 |
533 class ParseError(Exception): | 44 try: |
534 """Exception raised on errors in parsing the command line.""" | 45 opts = {} |
535 | |
536 def main(argv): | |
537 try: | 46 try: |
538 opts = {} | 47 args = fancyopts.fancyopts(sys.argv[1:], options, opts) |
539 try: | 48 except fancyopts.getopt.GetoptError, e: |
540 args = fancyopts.fancyopts(argv[1:], options, opts) | 49 raise ParseError(e) |
541 except fancyopts.getopt.GetoptError, e: | 50 if opts['help']: |
542 raise ParseError(e) | |
543 if opts['help']: | |
544 showhelp() | |
545 return 0 | |
546 if len(args) != 3: | |
547 raise ParseError(_('wrong number of arguments')) | |
548 return simplemerge(*args, **opts) | |
549 except ParseError, e: | |
550 sys.stdout.write("%s: %s\n" % (sys.argv[0], e)) | |
551 showhelp() | 51 showhelp() |
552 return 1 | 52 sys.exit(0) |
553 except util.Abort, e: | 53 if len(args) != 3: |
554 sys.stderr.write("abort: %s\n" % e) | 54 raise ParseError(_('wrong number of arguments')) |
555 return 255 | 55 sys.exit(simplemerge.simplemerge(*args, **opts)) |
556 except KeyboardInterrupt: | 56 except ParseError, e: |
557 return 255 | 57 sys.stdout.write("%s: %s\n" % (sys.argv[0], e)) |
558 | 58 showhelp() |
559 if __name__ == '__main__': | 59 sys.exit(1) |
560 import sys | 60 except util.Abort, e: |
561 import os | 61 sys.stderr.write("abort: %s\n" % e) |
562 sys.exit(main(sys.argv)) | 62 sys.exit(255) |
63 except KeyboardInterrupt: | |
64 sys.exit(255) |