##// END OF EJS Templates
filemerge: extract `_picklabels` as a helper function...
Phil Cohen -
r33829:aa6c290a default
parent child Browse files
Show More
@@ -1,504 +1,506 b''
1 1 # Copyright (C) 2004, 2005 Canonical Ltd
2 2 #
3 3 # This program is free software; you can redistribute it and/or modify
4 4 # it under the terms of the GNU General Public License as published by
5 5 # the Free Software Foundation; either version 2 of the License, or
6 6 # (at your option) any later version.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU General Public License
14 14 # along with this program; if not, see <http://www.gnu.org/licenses/>.
15 15
16 16 # mbp: "you know that thing where cvs gives you conflict markers?"
17 17 # s: "i hate that."
18 18
19 19 from __future__ import absolute_import
20 20
21 21 import os
22 22
23 23 from .i18n import _
24 24 from . import (
25 25 error,
26 26 mdiff,
27 27 pycompat,
28 28 util,
29 29 vfs as vfsmod,
30 30 )
31 31
32 32 class CantReprocessAndShowBase(Exception):
33 33 pass
34 34
35 35 def intersect(ra, rb):
36 36 """Given two ranges return the range where they intersect or None.
37 37
38 38 >>> intersect((0, 10), (0, 6))
39 39 (0, 6)
40 40 >>> intersect((0, 10), (5, 15))
41 41 (5, 10)
42 42 >>> intersect((0, 10), (10, 15))
43 43 >>> intersect((0, 9), (10, 15))
44 44 >>> intersect((0, 9), (7, 15))
45 45 (7, 9)
46 46 """
47 47 assert ra[0] <= ra[1]
48 48 assert rb[0] <= rb[1]
49 49
50 50 sa = max(ra[0], rb[0])
51 51 sb = min(ra[1], rb[1])
52 52 if sa < sb:
53 53 return sa, sb
54 54 else:
55 55 return None
56 56
57 57 def compare_range(a, astart, aend, b, bstart, bend):
58 58 """Compare a[astart:aend] == b[bstart:bend], without slicing.
59 59 """
60 60 if (aend - astart) != (bend - bstart):
61 61 return False
62 62 for ia, ib in zip(xrange(astart, aend), xrange(bstart, bend)):
63 63 if a[ia] != b[ib]:
64 64 return False
65 65 else:
66 66 return True
67 67
68 68 class Merge3Text(object):
69 69 """3-way merge of texts.
70 70
71 71 Given strings BASE, OTHER, THIS, tries to produce a combined text
72 72 incorporating the changes from both BASE->OTHER and BASE->THIS."""
73 73 def __init__(self, basetext, atext, btext, base=None, a=None, b=None):
74 74 self.basetext = basetext
75 75 self.atext = atext
76 76 self.btext = btext
77 77 if base is None:
78 78 base = mdiff.splitnewlines(basetext)
79 79 if a is None:
80 80 a = mdiff.splitnewlines(atext)
81 81 if b is None:
82 82 b = mdiff.splitnewlines(btext)
83 83 self.base = base
84 84 self.a = a
85 85 self.b = b
86 86
87 87 def merge_lines(self,
88 88 name_a=None,
89 89 name_b=None,
90 90 name_base=None,
91 91 start_marker='<<<<<<<',
92 92 mid_marker='=======',
93 93 end_marker='>>>>>>>',
94 94 base_marker=None,
95 95 localorother=None,
96 96 minimize=False):
97 97 """Return merge in cvs-like form.
98 98 """
99 99 self.conflicts = False
100 100 newline = '\n'
101 101 if len(self.a) > 0:
102 102 if self.a[0].endswith('\r\n'):
103 103 newline = '\r\n'
104 104 elif self.a[0].endswith('\r'):
105 105 newline = '\r'
106 106 if name_a and start_marker:
107 107 start_marker = start_marker + ' ' + name_a
108 108 if name_b and end_marker:
109 109 end_marker = end_marker + ' ' + name_b
110 110 if name_base and base_marker:
111 111 base_marker = base_marker + ' ' + name_base
112 112 merge_regions = self.merge_regions()
113 113 if minimize:
114 114 merge_regions = self.minimize(merge_regions)
115 115 for t in merge_regions:
116 116 what = t[0]
117 117 if what == 'unchanged':
118 118 for i in range(t[1], t[2]):
119 119 yield self.base[i]
120 120 elif what == 'a' or what == 'same':
121 121 for i in range(t[1], t[2]):
122 122 yield self.a[i]
123 123 elif what == 'b':
124 124 for i in range(t[1], t[2]):
125 125 yield self.b[i]
126 126 elif what == 'conflict':
127 127 if localorother == 'local':
128 128 for i in range(t[3], t[4]):
129 129 yield self.a[i]
130 130 elif localorother == 'other':
131 131 for i in range(t[5], t[6]):
132 132 yield self.b[i]
133 133 else:
134 134 self.conflicts = True
135 135 if start_marker is not None:
136 136 yield start_marker + newline
137 137 for i in range(t[3], t[4]):
138 138 yield self.a[i]
139 139 if base_marker is not None:
140 140 yield base_marker + newline
141 141 for i in range(t[1], t[2]):
142 142 yield self.base[i]
143 143 if mid_marker is not None:
144 144 yield mid_marker + newline
145 145 for i in range(t[5], t[6]):
146 146 yield self.b[i]
147 147 if end_marker is not None:
148 148 yield end_marker + newline
149 149 else:
150 150 raise ValueError(what)
151 151
152 152 def merge_groups(self):
153 153 """Yield sequence of line groups. Each one is a tuple:
154 154
155 155 'unchanged', lines
156 156 Lines unchanged from base
157 157
158 158 'a', lines
159 159 Lines taken from a
160 160
161 161 'same', lines
162 162 Lines taken from a (and equal to b)
163 163
164 164 'b', lines
165 165 Lines taken from b
166 166
167 167 'conflict', base_lines, a_lines, b_lines
168 168 Lines from base were changed to either a or b and conflict.
169 169 """
170 170 for t in self.merge_regions():
171 171 what = t[0]
172 172 if what == 'unchanged':
173 173 yield what, self.base[t[1]:t[2]]
174 174 elif what == 'a' or what == 'same':
175 175 yield what, self.a[t[1]:t[2]]
176 176 elif what == 'b':
177 177 yield what, self.b[t[1]:t[2]]
178 178 elif what == 'conflict':
179 179 yield (what,
180 180 self.base[t[1]:t[2]],
181 181 self.a[t[3]:t[4]],
182 182 self.b[t[5]:t[6]])
183 183 else:
184 184 raise ValueError(what)
185 185
186 186 def merge_regions(self):
187 187 """Return sequences of matching and conflicting regions.
188 188
189 189 This returns tuples, where the first value says what kind we
190 190 have:
191 191
192 192 'unchanged', start, end
193 193 Take a region of base[start:end]
194 194
195 195 'same', astart, aend
196 196 b and a are different from base but give the same result
197 197
198 198 'a', start, end
199 199 Non-clashing insertion from a[start:end]
200 200
201 201 'conflict', zstart, zend, astart, aend, bstart, bend
202 202 Conflict between a and b, with z as common ancestor
203 203
204 204 Method is as follows:
205 205
206 206 The two sequences align only on regions which match the base
207 207 and both descendants. These are found by doing a two-way diff
208 208 of each one against the base, and then finding the
209 209 intersections between those regions. These "sync regions"
210 210 are by definition unchanged in both and easily dealt with.
211 211
212 212 The regions in between can be in any of three cases:
213 213 conflicted, or changed on only one side.
214 214 """
215 215
216 216 # section a[0:ia] has been disposed of, etc
217 217 iz = ia = ib = 0
218 218
219 219 for region in self.find_sync_regions():
220 220 zmatch, zend, amatch, aend, bmatch, bend = region
221 221 #print 'match base [%d:%d]' % (zmatch, zend)
222 222
223 223 matchlen = zend - zmatch
224 224 assert matchlen >= 0
225 225 assert matchlen == (aend - amatch)
226 226 assert matchlen == (bend - bmatch)
227 227
228 228 len_a = amatch - ia
229 229 len_b = bmatch - ib
230 230 len_base = zmatch - iz
231 231 assert len_a >= 0
232 232 assert len_b >= 0
233 233 assert len_base >= 0
234 234
235 235 #print 'unmatched a=%d, b=%d' % (len_a, len_b)
236 236
237 237 if len_a or len_b:
238 238 # try to avoid actually slicing the lists
239 239 equal_a = compare_range(self.a, ia, amatch,
240 240 self.base, iz, zmatch)
241 241 equal_b = compare_range(self.b, ib, bmatch,
242 242 self.base, iz, zmatch)
243 243 same = compare_range(self.a, ia, amatch,
244 244 self.b, ib, bmatch)
245 245
246 246 if same:
247 247 yield 'same', ia, amatch
248 248 elif equal_a and not equal_b:
249 249 yield 'b', ib, bmatch
250 250 elif equal_b and not equal_a:
251 251 yield 'a', ia, amatch
252 252 elif not equal_a and not equal_b:
253 253 yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch
254 254 else:
255 255 raise AssertionError("can't handle a=b=base but unmatched")
256 256
257 257 ia = amatch
258 258 ib = bmatch
259 259 iz = zmatch
260 260
261 261 # if the same part of the base was deleted on both sides
262 262 # that's OK, we can just skip it.
263 263
264 264
265 265 if matchlen > 0:
266 266 assert ia == amatch
267 267 assert ib == bmatch
268 268 assert iz == zmatch
269 269
270 270 yield 'unchanged', zmatch, zend
271 271 iz = zend
272 272 ia = aend
273 273 ib = bend
274 274
275 275 def minimize(self, merge_regions):
276 276 """Trim conflict regions of lines where A and B sides match.
277 277
278 278 Lines where both A and B have made the same changes at the beginning
279 279 or the end of each merge region are eliminated from the conflict
280 280 region and are instead considered the same.
281 281 """
282 282 for region in merge_regions:
283 283 if region[0] != "conflict":
284 284 yield region
285 285 continue
286 286 issue, z1, z2, a1, a2, b1, b2 = region
287 287 alen = a2 - a1
288 288 blen = b2 - b1
289 289
290 290 # find matches at the front
291 291 ii = 0
292 292 while ii < alen and ii < blen and \
293 293 self.a[a1 + ii] == self.b[b1 + ii]:
294 294 ii += 1
295 295 startmatches = ii
296 296
297 297 # find matches at the end
298 298 ii = 0
299 299 while ii < alen and ii < blen and \
300 300 self.a[a2 - ii - 1] == self.b[b2 - ii - 1]:
301 301 ii += 1
302 302 endmatches = ii
303 303
304 304 if startmatches > 0:
305 305 yield 'same', a1, a1 + startmatches
306 306
307 307 yield ('conflict', z1, z2,
308 308 a1 + startmatches, a2 - endmatches,
309 309 b1 + startmatches, b2 - endmatches)
310 310
311 311 if endmatches > 0:
312 312 yield 'same', a2 - endmatches, a2
313 313
314 314 def find_sync_regions(self):
315 315 """Return a list of sync regions, where both descendants match the base.
316 316
317 317 Generates a list of (base1, base2, a1, a2, b1, b2). There is
318 318 always a zero-length sync region at the end of all the files.
319 319 """
320 320
321 321 ia = ib = 0
322 322 amatches = mdiff.get_matching_blocks(self.basetext, self.atext)
323 323 bmatches = mdiff.get_matching_blocks(self.basetext, self.btext)
324 324 len_a = len(amatches)
325 325 len_b = len(bmatches)
326 326
327 327 sl = []
328 328
329 329 while ia < len_a and ib < len_b:
330 330 abase, amatch, alen = amatches[ia]
331 331 bbase, bmatch, blen = bmatches[ib]
332 332
333 333 # there is an unconflicted block at i; how long does it
334 334 # extend? until whichever one ends earlier.
335 335 i = intersect((abase, abase + alen), (bbase, bbase + blen))
336 336 if i:
337 337 intbase = i[0]
338 338 intend = i[1]
339 339 intlen = intend - intbase
340 340
341 341 # found a match of base[i[0], i[1]]; this may be less than
342 342 # the region that matches in either one
343 343 assert intlen <= alen
344 344 assert intlen <= blen
345 345 assert abase <= intbase
346 346 assert bbase <= intbase
347 347
348 348 asub = amatch + (intbase - abase)
349 349 bsub = bmatch + (intbase - bbase)
350 350 aend = asub + intlen
351 351 bend = bsub + intlen
352 352
353 353 assert self.base[intbase:intend] == self.a[asub:aend], \
354 354 (self.base[intbase:intend], self.a[asub:aend])
355 355
356 356 assert self.base[intbase:intend] == self.b[bsub:bend]
357 357
358 358 sl.append((intbase, intend,
359 359 asub, aend,
360 360 bsub, bend))
361 361
362 362 # advance whichever one ends first in the base text
363 363 if (abase + alen) < (bbase + blen):
364 364 ia += 1
365 365 else:
366 366 ib += 1
367 367
368 368 intbase = len(self.base)
369 369 abase = len(self.a)
370 370 bbase = len(self.b)
371 371 sl.append((intbase, intbase, abase, abase, bbase, bbase))
372 372
373 373 return sl
374 374
375 375 def find_unconflicted(self):
376 376 """Return a list of ranges in base that are not conflicted."""
377 377 am = mdiff.get_matching_blocks(self.basetext, self.atext)
378 378 bm = mdiff.get_matching_blocks(self.basetext, self.btext)
379 379
380 380 unc = []
381 381
382 382 while am and bm:
383 383 # there is an unconflicted block at i; how long does it
384 384 # extend? until whichever one ends earlier.
385 385 a1 = am[0][0]
386 386 a2 = a1 + am[0][2]
387 387 b1 = bm[0][0]
388 388 b2 = b1 + bm[0][2]
389 389 i = intersect((a1, a2), (b1, b2))
390 390 if i:
391 391 unc.append(i)
392 392
393 393 if a2 < b2:
394 394 del am[0]
395 395 else:
396 396 del bm[0]
397 397
398 398 return unc
399 399
400 400 def _verifytext(text, path, ui, opts):
401 401 """verifies that text is non-binary (unless opts[text] is passed,
402 402 then we just warn)"""
403 403 if util.binary(text):
404 404 msg = _("%s looks like a binary file.") % path
405 405 if not opts.get('quiet'):
406 406 ui.warn(_('warning: %s\n') % msg)
407 407 if not opts.get('text'):
408 408 raise error.Abort(msg)
409 409 return text
410 410
411 def _picklabels(defaults, overrides):
412 name_a, name_b, name_base = defaults
413
414 if len(overrides) > 0:
415 name_a = overrides[0]
416 if len(overrides) > 1:
417 name_b = overrides[1]
418 if len(overrides) > 2:
419 name_base = overrides[2]
420 if len(overrides) > 3:
421 raise error.Abort(_("can only specify three labels."))
422
423 return [name_a, name_b, name_base]
424
411 425 def simplemerge(ui, localfile, basefile, otherfile,
412 426 localctx=None, basectx=None, otherctx=None, repo=None, **opts):
413 427 """Performs the simplemerge algorithm.
414 428
415 429 {local|base|other}ctx are optional. If passed, they (local/base/other) will
416 430 be read from and the merge result written to (local). You should pass
417 431 explicit labels in this mode since the default is to use the file paths."""
418 432 def readfile(filename):
419 433 f = open(filename, "rb")
420 434 text = f.read()
421 435 f.close()
422 436 return _verifytext(text, filename, ui, opts)
423 437
424 438 def readctx(ctx):
425 439 if not ctx:
426 440 return None
427 441 if not repo:
428 442 raise error.ProgrammingError('simplemerge: repo must be passed if '
429 443 'using contexts')
430 444 # `wwritedata` is used to get the post-filter data from `ctx` (i.e.,
431 445 # what would have been in the working copy). Since merges were run in
432 446 # the working copy, and thus used post-filter data, we do the same to
433 447 # maintain behavior.
434 448 return repo.wwritedata(ctx.path(),
435 449 _verifytext(ctx.data(), ctx.path(), ui, opts))
436 450
437 451 class ctxwriter(object):
438 452 def __init__(self, ctx):
439 453 self.ctx = ctx
440 454 self.text = ""
441 455
442 456 def write(self, text):
443 457 self.text += text
444 458
445 459 def close(self):
446 460 self.ctx.write(self.text, self.ctx.flags())
447 461
448 462 mode = opts.get('mode','merge')
449 if mode == 'union':
450 name_a = None
451 name_b = None
452 name_base = None
453 else:
454 name_a = localfile
455 name_b = otherfile
456 name_base = None
457 labels = opts.get('label', [])
458 if len(labels) > 0:
459 name_a = labels[0]
460 if len(labels) > 1:
461 name_b = labels[1]
462 if len(labels) > 2:
463 name_base = labels[2]
464 if len(labels) > 3:
465 raise error.Abort(_("can only specify three labels."))
463 name_a, name_b, name_base = None, None, None
464 if mode != 'union':
465 name_a, name_b, name_base = _picklabels([localfile,
466 otherfile, None],
467 opts.get('label', []))
466 468
467 469 try:
468 470 localtext = readctx(localctx) if localctx else readfile(localfile)
469 471 basetext = readctx(basectx) if basectx else readfile(basefile)
470 472 othertext = readctx(otherctx) if otherctx else readfile(otherfile)
471 473 except error.Abort:
472 474 return 1
473 475
474 476 if opts.get('print'):
475 477 out = ui.fout
476 478 elif localctx:
477 479 out = ctxwriter(localctx)
478 480 else:
479 481 localfile = os.path.realpath(localfile)
480 482 opener = vfsmod.vfs(os.path.dirname(localfile))
481 483 out = opener(os.path.basename(localfile), "w", atomictemp=True)
482 484
483 485 m3 = Merge3Text(basetext, localtext, othertext)
484 486 extrakwargs = {
485 487 "localorother": opts.get("localorother", None),
486 488 'minimize': True,
487 489 }
488 490 if mode == 'union':
489 491 extrakwargs['start_marker'] = None
490 492 extrakwargs['mid_marker'] = None
491 493 extrakwargs['end_marker'] = None
492 494 elif name_base is not None:
493 495 extrakwargs['base_marker'] = '|||||||'
494 496 extrakwargs['name_base'] = name_base
495 497 extrakwargs['minimize'] = False
496 498 for line in m3.merge_lines(name_a=name_a, name_b=name_b,
497 499 **pycompat.strkwargs(extrakwargs)):
498 500 out.write(line)
499 501
500 502 if not opts.get('print'):
501 503 out.close()
502 504
503 505 if m3.conflicts and not mode == 'union':
504 506 return 1
General Comments 0
You need to be logged in to leave comments. Login now