##// END OF EJS Templates
simplemerge: add optional context parameters to simplemerge...
Phil Cohen -
r33825:b3571dc0 default
parent child Browse files
Show More
@@ -1,472 +1,473 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 simplemerge(ui, local, base, other, **opts):
411 def simplemerge(ui, localfile, basefile, otherfile,
412 localctx=None, basectx=None, otherctx=None, repo=None, **opts):
412 413 def readfile(filename):
413 414 f = open(filename, "rb")
414 415 text = f.read()
415 416 f.close()
416 417 return _verifytext(text, filename, ui, opts)
417 418
418 419 mode = opts.get('mode','merge')
419 420 if mode == 'union':
420 421 name_a = None
421 422 name_b = None
422 423 name_base = None
423 424 else:
424 name_a = local
425 name_b = other
425 name_a = localfile
426 name_b = otherfile
426 427 name_base = None
427 428 labels = opts.get('label', [])
428 429 if len(labels) > 0:
429 430 name_a = labels[0]
430 431 if len(labels) > 1:
431 432 name_b = labels[1]
432 433 if len(labels) > 2:
433 434 name_base = labels[2]
434 435 if len(labels) > 3:
435 436 raise error.Abort(_("can only specify three labels."))
436 437
437 438 try:
438 localtext = readfile(local)
439 basetext = readfile(base)
440 othertext = readfile(other)
439 localtext = readfile(localfile)
440 basetext = readfile(basefile)
441 othertext = readfile(otherfile)
441 442 except error.Abort:
442 443 return 1
443 444
444 local = os.path.realpath(local)
445 localfile = os.path.realpath(localfile)
445 446 if not opts.get('print'):
446 opener = vfsmod.vfs(os.path.dirname(local))
447 out = opener(os.path.basename(local), "w", atomictemp=True)
447 opener = vfsmod.vfs(os.path.dirname(localfile))
448 out = opener(os.path.basename(localfile), "w", atomictemp=True)
448 449 else:
449 450 out = ui.fout
450 451
451 452 m3 = Merge3Text(basetext, localtext, othertext)
452 453 extrakwargs = {
453 454 "localorother": opts.get("localorother", None),
454 455 'minimize': True,
455 456 }
456 457 if mode == 'union':
457 458 extrakwargs['start_marker'] = None
458 459 extrakwargs['mid_marker'] = None
459 460 extrakwargs['end_marker'] = None
460 461 elif name_base is not None:
461 462 extrakwargs['base_marker'] = '|||||||'
462 463 extrakwargs['name_base'] = name_base
463 464 extrakwargs['minimize'] = False
464 465 for line in m3.merge_lines(name_a=name_a, name_b=name_b,
465 466 **pycompat.strkwargs(extrakwargs)):
466 467 out.write(line)
467 468
468 469 if not opts.get('print'):
469 470 out.close()
470 471
471 472 if m3.conflicts and not mode == 'union':
472 473 return 1
General Comments 0
You need to be logged in to leave comments. Login now