##// END OF EJS Templates
merge: introduce method to minimize merge regions...
Ryan McElroy -
r28071:261324dd default
parent child Browse files
Show More
@@ -1,420 +1,459 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 import sys
23 23
24 24 from .i18n import _
25 25 from . import (
26 26 error,
27 27 mdiff,
28 28 scmutil,
29 29 util,
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 """Return merge in cvs-like form.
97 97 """
98 98 self.conflicts = False
99 99 newline = '\n'
100 100 if len(self.a) > 0:
101 101 if self.a[0].endswith('\r\n'):
102 102 newline = '\r\n'
103 103 elif self.a[0].endswith('\r'):
104 104 newline = '\r'
105 105 if name_a and start_marker:
106 106 start_marker = start_marker + ' ' + name_a
107 107 if name_b and end_marker:
108 108 end_marker = end_marker + ' ' + name_b
109 109 if name_base and base_marker:
110 110 base_marker = base_marker + ' ' + name_base
111 111 merge_regions = self.merge_regions()
112 112 for t in merge_regions:
113 113 what = t[0]
114 114 if what == 'unchanged':
115 115 for i in range(t[1], t[2]):
116 116 yield self.base[i]
117 117 elif what == 'a' or what == 'same':
118 118 for i in range(t[1], t[2]):
119 119 yield self.a[i]
120 120 elif what == 'b':
121 121 for i in range(t[1], t[2]):
122 122 yield self.b[i]
123 123 elif what == 'conflict':
124 124 if localorother == 'local':
125 125 for i in range(t[3], t[4]):
126 126 yield self.a[i]
127 127 elif localorother == 'other':
128 128 for i in range(t[5], t[6]):
129 129 yield self.b[i]
130 130 else:
131 131 self.conflicts = True
132 132 if start_marker is not None:
133 133 yield start_marker + newline
134 134 for i in range(t[3], t[4]):
135 135 yield self.a[i]
136 136 if base_marker is not None:
137 137 yield base_marker + newline
138 138 for i in range(t[1], t[2]):
139 139 yield self.base[i]
140 140 if mid_marker is not None:
141 141 yield mid_marker + newline
142 142 for i in range(t[5], t[6]):
143 143 yield self.b[i]
144 144 if end_marker is not None:
145 145 yield end_marker + newline
146 146 else:
147 147 raise ValueError(what)
148 148
149 149 def merge_groups(self):
150 150 """Yield sequence of line groups. Each one is a tuple:
151 151
152 152 'unchanged', lines
153 153 Lines unchanged from base
154 154
155 155 'a', lines
156 156 Lines taken from a
157 157
158 158 'same', lines
159 159 Lines taken from a (and equal to b)
160 160
161 161 'b', lines
162 162 Lines taken from b
163 163
164 164 'conflict', base_lines, a_lines, b_lines
165 165 Lines from base were changed to either a or b and conflict.
166 166 """
167 167 for t in self.merge_regions():
168 168 what = t[0]
169 169 if what == 'unchanged':
170 170 yield what, self.base[t[1]:t[2]]
171 171 elif what == 'a' or what == 'same':
172 172 yield what, self.a[t[1]:t[2]]
173 173 elif what == 'b':
174 174 yield what, self.b[t[1]:t[2]]
175 175 elif what == 'conflict':
176 176 yield (what,
177 177 self.base[t[1]:t[2]],
178 178 self.a[t[3]:t[4]],
179 179 self.b[t[5]:t[6]])
180 180 else:
181 181 raise ValueError(what)
182 182
183 183 def merge_regions(self):
184 184 """Return sequences of matching and conflicting regions.
185 185
186 186 This returns tuples, where the first value says what kind we
187 187 have:
188 188
189 189 'unchanged', start, end
190 190 Take a region of base[start:end]
191 191
192 192 'same', astart, aend
193 193 b and a are different from base but give the same result
194 194
195 195 'a', start, end
196 196 Non-clashing insertion from a[start:end]
197 197
198 198 'conflict', zstart, zend, astart, aend, bstart, bend
199 199 Conflict between a and b, with z as common ancestor
200 200
201 201 Method is as follows:
202 202
203 203 The two sequences align only on regions which match the base
204 204 and both descendants. These are found by doing a two-way diff
205 205 of each one against the base, and then finding the
206 206 intersections between those regions. These "sync regions"
207 207 are by definition unchanged in both and easily dealt with.
208 208
209 209 The regions in between can be in any of three cases:
210 210 conflicted, or changed on only one side.
211 211 """
212 212
213 213 # section a[0:ia] has been disposed of, etc
214 214 iz = ia = ib = 0
215 215
216 216 for region in self.find_sync_regions():
217 217 zmatch, zend, amatch, aend, bmatch, bend = region
218 218 #print 'match base [%d:%d]' % (zmatch, zend)
219 219
220 220 matchlen = zend - zmatch
221 221 assert matchlen >= 0
222 222 assert matchlen == (aend - amatch)
223 223 assert matchlen == (bend - bmatch)
224 224
225 225 len_a = amatch - ia
226 226 len_b = bmatch - ib
227 227 len_base = zmatch - iz
228 228 assert len_a >= 0
229 229 assert len_b >= 0
230 230 assert len_base >= 0
231 231
232 232 #print 'unmatched a=%d, b=%d' % (len_a, len_b)
233 233
234 234 if len_a or len_b:
235 235 # try to avoid actually slicing the lists
236 236 equal_a = compare_range(self.a, ia, amatch,
237 237 self.base, iz, zmatch)
238 238 equal_b = compare_range(self.b, ib, bmatch,
239 239 self.base, iz, zmatch)
240 240 same = compare_range(self.a, ia, amatch,
241 241 self.b, ib, bmatch)
242 242
243 243 if same:
244 244 yield 'same', ia, amatch
245 245 elif equal_a and not equal_b:
246 246 yield 'b', ib, bmatch
247 247 elif equal_b and not equal_a:
248 248 yield 'a', ia, amatch
249 249 elif not equal_a and not equal_b:
250 250 yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch
251 251 else:
252 252 raise AssertionError("can't handle a=b=base but unmatched")
253 253
254 254 ia = amatch
255 255 ib = bmatch
256 256 iz = zmatch
257 257
258 258 # if the same part of the base was deleted on both sides
259 259 # that's OK, we can just skip it.
260 260
261 261
262 262 if matchlen > 0:
263 263 assert ia == amatch
264 264 assert ib == bmatch
265 265 assert iz == zmatch
266 266
267 267 yield 'unchanged', zmatch, zend
268 268 iz = zend
269 269 ia = aend
270 270 ib = bend
271 271
272 def minimize(self, merge_regions):
273 """Trim conflict regions of lines where A and B sides match.
274
275 Lines where both A and B have made the same changes at the begining
276 or the end of each merge region are eliminated from the conflict
277 region and are instead considered the same.
278 """
279 for region in merge_regions:
280 if region[0] != "conflict":
281 yield region
282 continue
283 issue, z1, z2, a1, a2, b1, b2 = region
284 alen = a2 - a1
285 blen = b2 - b1
286
287 # find matches at the front
288 ii = 0
289 while ii < alen and ii < blen and \
290 self.a[a1 + ii] == self.b[b1 + ii]:
291 ii += 1
292 startmatches = ii
293
294 # find matches at the end
295 ii = 0
296 while ii < alen and ii < blen and \
297 self.a[a2 - ii - 1] == self.b[b2 - ii - 1]:
298 ii += 1
299 endmatches = ii
300
301 if startmatches > 0:
302 yield 'same', a1, a1 + startmatches
303
304 yield ('conflict', z1, z2,
305 a1 + startmatches, a2 - endmatches,
306 b1 + startmatches, b2 - endmatches)
307
308 if endmatches > 0:
309 yield 'same', a2 - endmatches, a2
310
272 311 def find_sync_regions(self):
273 312 """Return a list of sync regions, where both descendants match the base.
274 313
275 314 Generates a list of (base1, base2, a1, a2, b1, b2). There is
276 315 always a zero-length sync region at the end of all the files.
277 316 """
278 317
279 318 ia = ib = 0
280 319 amatches = mdiff.get_matching_blocks(self.basetext, self.atext)
281 320 bmatches = mdiff.get_matching_blocks(self.basetext, self.btext)
282 321 len_a = len(amatches)
283 322 len_b = len(bmatches)
284 323
285 324 sl = []
286 325
287 326 while ia < len_a and ib < len_b:
288 327 abase, amatch, alen = amatches[ia]
289 328 bbase, bmatch, blen = bmatches[ib]
290 329
291 330 # there is an unconflicted block at i; how long does it
292 331 # extend? until whichever one ends earlier.
293 332 i = intersect((abase, abase + alen), (bbase, bbase + blen))
294 333 if i:
295 334 intbase = i[0]
296 335 intend = i[1]
297 336 intlen = intend - intbase
298 337
299 338 # found a match of base[i[0], i[1]]; this may be less than
300 339 # the region that matches in either one
301 340 assert intlen <= alen
302 341 assert intlen <= blen
303 342 assert abase <= intbase
304 343 assert bbase <= intbase
305 344
306 345 asub = amatch + (intbase - abase)
307 346 bsub = bmatch + (intbase - bbase)
308 347 aend = asub + intlen
309 348 bend = bsub + intlen
310 349
311 350 assert self.base[intbase:intend] == self.a[asub:aend], \
312 351 (self.base[intbase:intend], self.a[asub:aend])
313 352
314 353 assert self.base[intbase:intend] == self.b[bsub:bend]
315 354
316 355 sl.append((intbase, intend,
317 356 asub, aend,
318 357 bsub, bend))
319 358
320 359 # advance whichever one ends first in the base text
321 360 if (abase + alen) < (bbase + blen):
322 361 ia += 1
323 362 else:
324 363 ib += 1
325 364
326 365 intbase = len(self.base)
327 366 abase = len(self.a)
328 367 bbase = len(self.b)
329 368 sl.append((intbase, intbase, abase, abase, bbase, bbase))
330 369
331 370 return sl
332 371
333 372 def find_unconflicted(self):
334 373 """Return a list of ranges in base that are not conflicted."""
335 374 am = mdiff.get_matching_blocks(self.basetext, self.atext)
336 375 bm = mdiff.get_matching_blocks(self.basetext, self.btext)
337 376
338 377 unc = []
339 378
340 379 while am and bm:
341 380 # there is an unconflicted block at i; how long does it
342 381 # extend? until whichever one ends earlier.
343 382 a1 = am[0][0]
344 383 a2 = a1 + am[0][2]
345 384 b1 = bm[0][0]
346 385 b2 = b1 + bm[0][2]
347 386 i = intersect((a1, a2), (b1, b2))
348 387 if i:
349 388 unc.append(i)
350 389
351 390 if a2 < b2:
352 391 del am[0]
353 392 else:
354 393 del bm[0]
355 394
356 395 return unc
357 396
358 397 def simplemerge(ui, local, base, other, **opts):
359 398 def readfile(filename):
360 399 f = open(filename, "rb")
361 400 text = f.read()
362 401 f.close()
363 402 if util.binary(text):
364 403 msg = _("%s looks like a binary file.") % filename
365 404 if not opts.get('quiet'):
366 405 ui.warn(_('warning: %s\n') % msg)
367 406 if not opts.get('text'):
368 407 raise error.Abort(msg)
369 408 return text
370 409
371 410 mode = opts.get('mode','merge')
372 411 if mode == 'union':
373 412 name_a = None
374 413 name_b = None
375 414 name_base = None
376 415 else:
377 416 name_a = local
378 417 name_b = other
379 418 name_base = None
380 419 labels = opts.get('label', [])
381 420 if len(labels) > 0:
382 421 name_a = labels[0]
383 422 if len(labels) > 1:
384 423 name_b = labels[1]
385 424 if len(labels) > 2:
386 425 name_base = labels[2]
387 426 if len(labels) > 3:
388 427 raise error.Abort(_("can only specify three labels."))
389 428
390 429 try:
391 430 localtext = readfile(local)
392 431 basetext = readfile(base)
393 432 othertext = readfile(other)
394 433 except error.Abort:
395 434 return 1
396 435
397 436 local = os.path.realpath(local)
398 437 if not opts.get('print'):
399 438 opener = scmutil.opener(os.path.dirname(local))
400 439 out = opener(os.path.basename(local), "w", atomictemp=True)
401 440 else:
402 441 out = sys.stdout
403 442
404 443 m3 = Merge3Text(basetext, localtext, othertext)
405 444 extrakwargs = {"localorother": opts.get("localorother", None)}
406 445 if mode == 'union':
407 446 extrakwargs['start_marker'] = None
408 447 extrakwargs['mid_marker'] = None
409 448 extrakwargs['end_marker'] = None
410 449 elif name_base is not None:
411 450 extrakwargs['base_marker'] = '|||||||'
412 451 extrakwargs['name_base'] = name_base
413 452 for line in m3.merge_lines(name_a=name_a, name_b=name_b, **extrakwargs):
414 453 out.write(line)
415 454
416 455 if not opts.get('print'):
417 456 out.close()
418 457
419 458 if m3.conflicts and not mode == 'union':
420 459 return 1
General Comments 0
You need to be logged in to leave comments. Login now