##// END OF EJS Templates
filemerge: add a before-merge callback to internal merge tools...
Siddharth Agarwal -
r26513:01224c28 default
parent child Browse files
Show More
@@ -1,547 +1,548 b''
1 1 # filemerge.py - file-level merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import filecmp
11 11 import os
12 12 import re
13 13 import tempfile
14 14
15 15 from .i18n import _
16 16 from .node import short
17 17
18 18 from . import (
19 19 error,
20 20 match,
21 21 simplemerge,
22 22 tagmerge,
23 23 templatekw,
24 24 templater,
25 25 util,
26 26 )
27 27
28 28 def _toolstr(ui, tool, part, default=""):
29 29 return ui.config("merge-tools", tool + "." + part, default)
30 30
31 31 def _toolbool(ui, tool, part, default=False):
32 32 return ui.configbool("merge-tools", tool + "." + part, default)
33 33
34 34 def _toollist(ui, tool, part, default=[]):
35 35 return ui.configlist("merge-tools", tool + "." + part, default)
36 36
37 37 internals = {}
38 38 # Merge tools to document.
39 39 internalsdoc = {}
40 40
41 def internaltool(name, trymerge, onfailure=None):
41 def internaltool(name, trymerge, onfailure=None, precheck=None):
42 42 '''return a decorator for populating internal merge tool table'''
43 43 def decorator(func):
44 44 fullname = ':' + name
45 45 func.__doc__ = "``%s``\n" % fullname + func.__doc__.strip()
46 46 internals[fullname] = func
47 47 internals['internal:' + name] = func
48 48 internalsdoc[fullname] = func
49 49 func.trymerge = trymerge
50 50 func.onfailure = onfailure
51 func.precheck = precheck
51 52 return func
52 53 return decorator
53 54
54 55 def _findtool(ui, tool):
55 56 if tool in internals:
56 57 return tool
57 58 return findexternaltool(ui, tool)
58 59
59 60 def findexternaltool(ui, tool):
60 61 for kn in ("regkey", "regkeyalt"):
61 62 k = _toolstr(ui, tool, kn)
62 63 if not k:
63 64 continue
64 65 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
65 66 if p:
66 67 p = util.findexe(p + _toolstr(ui, tool, "regappend"))
67 68 if p:
68 69 return p
69 70 exe = _toolstr(ui, tool, "executable", tool)
70 71 return util.findexe(util.expandpath(exe))
71 72
72 73 def _picktool(repo, ui, path, binary, symlink):
73 74 def check(tool, pat, symlink, binary):
74 75 tmsg = tool
75 76 if pat:
76 77 tmsg += " specified for " + pat
77 78 if not _findtool(ui, tool):
78 79 if pat: # explicitly requested tool deserves a warning
79 80 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
80 81 else: # configured but non-existing tools are more silent
81 82 ui.note(_("couldn't find merge tool %s\n") % tmsg)
82 83 elif symlink and not _toolbool(ui, tool, "symlink"):
83 84 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
84 85 elif binary and not _toolbool(ui, tool, "binary"):
85 86 ui.warn(_("tool %s can't handle binary\n") % tmsg)
86 87 elif not util.gui() and _toolbool(ui, tool, "gui"):
87 88 ui.warn(_("tool %s requires a GUI\n") % tmsg)
88 89 else:
89 90 return True
90 91 return False
91 92
92 93 # internal config: ui.forcemerge
93 94 # forcemerge comes from command line arguments, highest priority
94 95 force = ui.config('ui', 'forcemerge')
95 96 if force:
96 97 toolpath = _findtool(ui, force)
97 98 if toolpath:
98 99 return (force, util.shellquote(toolpath))
99 100 else:
100 101 # mimic HGMERGE if given tool not found
101 102 return (force, force)
102 103
103 104 # HGMERGE takes next precedence
104 105 hgmerge = os.environ.get("HGMERGE")
105 106 if hgmerge:
106 107 return (hgmerge, hgmerge)
107 108
108 109 # then patterns
109 110 for pat, tool in ui.configitems("merge-patterns"):
110 111 mf = match.match(repo.root, '', [pat])
111 112 if mf(path) and check(tool, pat, symlink, False):
112 113 toolpath = _findtool(ui, tool)
113 114 return (tool, util.shellquote(toolpath))
114 115
115 116 # then merge tools
116 117 tools = {}
117 118 for k, v in ui.configitems("merge-tools"):
118 119 t = k.split('.')[0]
119 120 if t not in tools:
120 121 tools[t] = int(_toolstr(ui, t, "priority", "0"))
121 122 names = tools.keys()
122 123 tools = sorted([(-p, t) for t, p in tools.items()])
123 124 uimerge = ui.config("ui", "merge")
124 125 if uimerge:
125 126 if uimerge not in names:
126 127 return (uimerge, uimerge)
127 128 tools.insert(0, (None, uimerge)) # highest priority
128 129 tools.append((None, "hgmerge")) # the old default, if found
129 130 for p, t in tools:
130 131 if check(t, None, symlink, binary):
131 132 toolpath = _findtool(ui, t)
132 133 return (t, util.shellquote(toolpath))
133 134
134 135 # internal merge or prompt as last resort
135 136 if symlink or binary:
136 137 return ":prompt", None
137 138 return ":merge", None
138 139
139 140 def _eoltype(data):
140 141 "Guess the EOL type of a file"
141 142 if '\0' in data: # binary
142 143 return None
143 144 if '\r\n' in data: # Windows
144 145 return '\r\n'
145 146 if '\r' in data: # Old Mac
146 147 return '\r'
147 148 if '\n' in data: # UNIX
148 149 return '\n'
149 150 return None # unknown
150 151
151 152 def _matcheol(file, origfile):
152 153 "Convert EOL markers in a file to match origfile"
153 154 tostyle = _eoltype(util.readfile(origfile))
154 155 if tostyle:
155 156 data = util.readfile(file)
156 157 style = _eoltype(data)
157 158 if style:
158 159 newdata = data.replace(style, tostyle)
159 160 if newdata != data:
160 161 util.writefile(file, newdata)
161 162
162 163 @internaltool('prompt', False)
163 164 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf):
164 165 """Asks the user which of the local or the other version to keep as
165 166 the merged version."""
166 167 ui = repo.ui
167 168 fd = fcd.path()
168 169
169 170 if ui.promptchoice(_(" no tool found to merge %s\n"
170 171 "keep (l)ocal or take (o)ther?"
171 172 "$$ &Local $$ &Other") % fd, 0):
172 173 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf)
173 174 else:
174 175 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf)
175 176
176 177 @internaltool('local', False)
177 178 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf):
178 179 """Uses the local version of files as the merged version."""
179 180 return 0
180 181
181 182 @internaltool('other', False)
182 183 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf):
183 184 """Uses the other version of files as the merged version."""
184 185 repo.wwrite(fcd.path(), fco.data(), fco.flags())
185 186 return 0
186 187
187 188 @internaltool('fail', False)
188 189 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf):
189 190 """
190 191 Rather than attempting to merge files that were modified on both
191 192 branches, it marks them as unresolved. The resolve command must be
192 193 used to resolve these conflicts."""
193 194 return 1
194 195
195 196 def _premerge(repo, toolconf, files, labels=None):
196 197 tool, toolpath, binary, symlink = toolconf
197 198 if symlink:
198 199 return 1
199 200 a, b, c, back = files
200 201
201 202 ui = repo.ui
202 203
203 204 validkeep = ['keep', 'keep-merge3']
204 205
205 206 # do we attempt to simplemerge first?
206 207 try:
207 208 premerge = _toolbool(ui, tool, "premerge", not binary)
208 209 except error.ConfigError:
209 210 premerge = _toolstr(ui, tool, "premerge").lower()
210 211 if premerge not in validkeep:
211 212 _valid = ', '.join(["'" + v + "'" for v in validkeep])
212 213 raise error.ConfigError(_("%s.premerge not valid "
213 214 "('%s' is neither boolean nor %s)") %
214 215 (tool, premerge, _valid))
215 216
216 217 if premerge:
217 218 if premerge == 'keep-merge3':
218 219 if not labels:
219 220 labels = _defaultconflictlabels
220 221 if len(labels) < 3:
221 222 labels.append('base')
222 223 r = simplemerge.simplemerge(ui, a, b, c, quiet=True, label=labels)
223 224 if not r:
224 225 ui.debug(" premerge successful\n")
225 226 return 0
226 227 if premerge not in validkeep:
227 228 util.copyfile(back, a) # restore from backup and try again
228 229 return 1 # continue merging
229 230
230 231 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
231 232 """
232 233 Uses the internal non-interactive simple merge algorithm for merging
233 234 files. It will fail if there are any conflicts and leave markers in
234 235 the partially merged file. Markers will have two sections, one for each side
235 236 of merge, unless mode equals 'union' which suppresses the markers."""
236 237 tool, toolpath, binary, symlink = toolconf
237 238 if symlink:
238 239 repo.ui.warn(_('warning: internal :merge cannot merge symlinks '
239 240 'for %s\n') % fcd.path())
240 241 return False, 1
241 242 r = _premerge(repo, toolconf, files, labels=labels)
242 243 if r:
243 244 a, b, c, back = files
244 245
245 246 ui = repo.ui
246 247
247 248 r = simplemerge.simplemerge(ui, a, b, c, label=labels, mode=mode)
248 249 return True, r
249 250 return False, 0
250 251
251 252 @internaltool('union', True,
252 253 _("merging %s incomplete! "
253 254 "(edit conflicts, then use 'hg resolve --mark')\n"))
254 255 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
255 256 """
256 257 Uses the internal non-interactive simple merge algorithm for merging
257 258 files. It will use both left and right sides for conflict regions.
258 259 No markers are inserted."""
259 260 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
260 261 files, labels, 'union')
261 262
262 263 @internaltool('merge', True,
263 264 _("merging %s incomplete! "
264 265 "(edit conflicts, then use 'hg resolve --mark')\n"))
265 266 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
266 267 """
267 268 Uses the internal non-interactive simple merge algorithm for merging
268 269 files. It will fail if there are any conflicts and leave markers in
269 270 the partially merged file. Markers will have two sections, one for each side
270 271 of merge."""
271 272 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
272 273 files, labels, 'merge')
273 274
274 275 @internaltool('merge3', True,
275 276 _("merging %s incomplete! "
276 277 "(edit conflicts, then use 'hg resolve --mark')\n"))
277 278 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
278 279 """
279 280 Uses the internal non-interactive simple merge algorithm for merging
280 281 files. It will fail if there are any conflicts and leave markers in
281 282 the partially merged file. Marker will have three sections, one from each
282 283 side of the merge and one for the base content."""
283 284 if not labels:
284 285 labels = _defaultconflictlabels
285 286 if len(labels) < 3:
286 287 labels.append('base')
287 288 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
288 289
289 290 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
290 291 labels=None, localorother=None):
291 292 """
292 293 Generic driver for _imergelocal and _imergeother
293 294 """
294 295 assert localorother is not None
295 296 tool, toolpath, binary, symlink = toolconf
296 297 if symlink:
297 298 repo.ui.warn(_('warning: :merge-%s cannot merge symlinks '
298 299 'for %s\n') % (localorother, fcd.path()))
299 300 return False, 1
300 301 a, b, c, back = files
301 302 r = simplemerge.simplemerge(repo.ui, a, b, c, label=labels,
302 303 localorother=localorother)
303 304 return True, r
304 305
305 306 @internaltool('merge-local', True)
306 307 def _imergelocal(*args, **kwargs):
307 308 """
308 309 Like :merge, but resolve all conflicts non-interactively in favor
309 310 of the local changes."""
310 311 success, status = _imergeauto(localorother='local', *args, **kwargs)
311 312 return success, status
312 313
313 314 @internaltool('merge-other', True)
314 315 def _imergeother(*args, **kwargs):
315 316 """
316 317 Like :merge, but resolve all conflicts non-interactively in favor
317 318 of the other changes."""
318 319 success, status = _imergeauto(localorother='other', *args, **kwargs)
319 320 return success, status
320 321
321 322 @internaltool('tagmerge', True,
322 323 _("automatic tag merging of %s failed! "
323 324 "(use 'hg resolve --tool :merge' or another merge "
324 325 "tool of your choice)\n"))
325 326 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
326 327 """
327 328 Uses the internal tag merge algorithm (experimental).
328 329 """
329 330 return tagmerge.merge(repo, fcd, fco, fca)
330 331
331 332 @internaltool('dump', True)
332 333 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
333 334 """
334 335 Creates three versions of the files to merge, containing the
335 336 contents of local, other and base. These files can then be used to
336 337 perform a merge manually. If the file to be merged is named
337 338 ``a.txt``, these files will accordingly be named ``a.txt.local``,
338 339 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
339 340 same directory as ``a.txt``."""
340 341 r = _premerge(repo, toolconf, files, labels=labels)
341 342 if r:
342 343 a, b, c, back = files
343 344
344 345 fd = fcd.path()
345 346
346 347 util.copyfile(a, a + ".local")
347 348 repo.wwrite(fd + ".other", fco.data(), fco.flags())
348 349 repo.wwrite(fd + ".base", fca.data(), fca.flags())
349 350 return False, r
350 351
351 352 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
352 353 r = _premerge(repo, toolconf, files, labels=labels)
353 354 if r:
354 355 tool, toolpath, binary, symlink = toolconf
355 356 a, b, c, back = files
356 357 out = ""
357 358 env = {'HG_FILE': fcd.path(),
358 359 'HG_MY_NODE': short(mynode),
359 360 'HG_OTHER_NODE': str(fco.changectx()),
360 361 'HG_BASE_NODE': str(fca.changectx()),
361 362 'HG_MY_ISLINK': 'l' in fcd.flags(),
362 363 'HG_OTHER_ISLINK': 'l' in fco.flags(),
363 364 'HG_BASE_ISLINK': 'l' in fca.flags(),
364 365 }
365 366
366 367 ui = repo.ui
367 368
368 369 args = _toolstr(ui, tool, "args", '$local $base $other')
369 370 if "$output" in args:
370 371 out, a = a, back # read input from backup, write to original
371 372 replace = {'local': a, 'base': b, 'other': c, 'output': out}
372 373 args = util.interpolate(r'\$', replace, args,
373 374 lambda s: util.shellquote(util.localpath(s)))
374 375 cmd = toolpath + ' ' + args
375 376 repo.ui.debug('launching merge tool: %s\n' % cmd)
376 377 r = ui.system(cmd, cwd=repo.root, environ=env)
377 378 repo.ui.debug('merge tool returned: %s\n' % r)
378 379 return True, r
379 380 return False, 0
380 381
381 382 def _formatconflictmarker(repo, ctx, template, label, pad):
382 383 """Applies the given template to the ctx, prefixed by the label.
383 384
384 385 Pad is the minimum width of the label prefix, so that multiple markers
385 386 can have aligned templated parts.
386 387 """
387 388 if ctx.node() is None:
388 389 ctx = ctx.p1()
389 390
390 391 props = templatekw.keywords.copy()
391 392 props['templ'] = template
392 393 props['ctx'] = ctx
393 394 props['repo'] = repo
394 395 templateresult = template('conflictmarker', **props)
395 396
396 397 label = ('%s:' % label).ljust(pad + 1)
397 398 mark = '%s %s' % (label, templater.stringify(templateresult))
398 399
399 400 if mark:
400 401 mark = mark.splitlines()[0] # split for safety
401 402
402 403 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
403 404 return util.ellipsis(mark, 80 - 8)
404 405
405 406 _defaultconflictmarker = ('{node|short} ' +
406 407 '{ifeq(tags, "tip", "", "{tags} ")}' +
407 408 '{if(bookmarks, "{bookmarks} ")}' +
408 409 '{ifeq(branch, "default", "", "{branch} ")}' +
409 410 '- {author|user}: {desc|firstline}')
410 411
411 412 _defaultconflictlabels = ['local', 'other']
412 413
413 414 def _formatlabels(repo, fcd, fco, fca, labels):
414 415 """Formats the given labels using the conflict marker template.
415 416
416 417 Returns a list of formatted labels.
417 418 """
418 419 cd = fcd.changectx()
419 420 co = fco.changectx()
420 421 ca = fca.changectx()
421 422
422 423 ui = repo.ui
423 424 template = ui.config('ui', 'mergemarkertemplate', _defaultconflictmarker)
424 425 tmpl = templater.templater(None, cache={'conflictmarker': template})
425 426
426 427 pad = max(len(l) for l in labels)
427 428
428 429 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
429 430 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
430 431 if len(labels) > 2:
431 432 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
432 433 return newlabels
433 434
434 435 def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
435 436 """perform a 3-way merge in the working directory
436 437
437 438 mynode = parent node before merge
438 439 orig = original local filename before merge
439 440 fco = other file context
440 441 fca = ancestor file context
441 442 fcd = local file context for current/destination file
442 443 """
443 444
444 445 if True:
445 446 def temp(prefix, ctx):
446 447 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
447 448 (fd, name) = tempfile.mkstemp(prefix=pre)
448 449 data = repo.wwritedata(ctx.path(), ctx.data())
449 450 f = os.fdopen(fd, "wb")
450 451 f.write(data)
451 452 f.close()
452 453 return name
453 454
454 455 if not fco.cmp(fcd): # files identical?
455 456 return None
456 457
457 458 ui = repo.ui
458 459 fd = fcd.path()
459 460 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
460 461 symlink = 'l' in fcd.flags() + fco.flags()
461 462 tool, toolpath = _picktool(repo, ui, fd, binary, symlink)
462 463 ui.debug("picked tool '%s' for %s (binary %s symlink %s)\n" %
463 464 (tool, fd, binary, symlink))
464 465
465 466 if tool in internals:
466 467 func = internals[tool]
467 468 trymerge = func.trymerge
468 469 onfailure = func.onfailure
469 470 else:
470 471 func = _xmerge
471 472 trymerge = True
472 473 onfailure = _("merging %s failed!\n")
473 474
474 475 toolconf = tool, toolpath, binary, symlink
475 476
476 477 if not trymerge:
477 478 return func(repo, mynode, orig, fcd, fco, fca, toolconf)
478 479
479 480 a = repo.wjoin(fd)
480 481 b = temp("base", fca)
481 482 c = temp("other", fco)
482 483 back = a + ".orig"
483 484 util.copyfile(a, back)
484 485
485 486 if orig != fco.path():
486 487 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
487 488 else:
488 489 ui.status(_("merging %s\n") % fd)
489 490
490 491 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
491 492
492 493 markerstyle = ui.config('ui', 'mergemarkers', 'basic')
493 494 if not labels:
494 495 labels = _defaultconflictlabels
495 496 if markerstyle != 'basic':
496 497 labels = _formatlabels(repo, fcd, fco, fca, labels)
497 498
498 499 needcheck, r = func(repo, mynode, orig, fcd, fco, fca, toolconf,
499 500 (a, b, c, back), labels=labels)
500 501 if not needcheck:
501 502 if r:
502 503 if onfailure:
503 504 ui.warn(onfailure % fd)
504 505 else:
505 506 util.unlink(back)
506 507
507 508 util.unlink(b)
508 509 util.unlink(c)
509 510 return r
510 511
511 512 if not r and (_toolbool(ui, tool, "checkconflicts") or
512 513 'conflicts' in _toollist(ui, tool, "check")):
513 514 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
514 515 re.MULTILINE):
515 516 r = 1
516 517
517 518 checked = False
518 519 if 'prompt' in _toollist(ui, tool, "check"):
519 520 checked = True
520 521 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
521 522 "$$ &Yes $$ &No") % fd, 1):
522 523 r = 1
523 524
524 525 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
525 526 'changed' in
526 527 _toollist(ui, tool, "check")):
527 528 if filecmp.cmp(a, back):
528 529 if ui.promptchoice(_(" output file %s appears unchanged\n"
529 530 "was merge successful (yn)?"
530 531 "$$ &Yes $$ &No") % fd, 1):
531 532 r = 1
532 533
533 534 if _toolbool(ui, tool, "fixeol"):
534 535 _matcheol(a, back)
535 536
536 537 if r:
537 538 if onfailure:
538 539 ui.warn(onfailure % fd)
539 540 else:
540 541 util.unlink(back)
541 542
542 543 util.unlink(b)
543 544 util.unlink(c)
544 545 return r
545 546
546 547 # tell hggettext to extract docstrings from these functions:
547 548 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now