##// END OF EJS Templates
filemerge: add the function to examine a capability of a internal tool...
FUJIWARA Katsunori -
r39159:4d7b1187 default
parent child Browse files
Show More
@@ -1,978 +1,984
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 contextlib
11 11 import os
12 12 import re
13 13 import shutil
14 14
15 15 from .i18n import _
16 16 from .node import nullid, short
17 17
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 formatter,
22 22 match,
23 23 pycompat,
24 24 registrar,
25 25 scmutil,
26 26 simplemerge,
27 27 tagmerge,
28 28 templatekw,
29 29 templater,
30 30 util,
31 31 )
32 32
33 33 from .utils import (
34 34 procutil,
35 35 stringutil,
36 36 )
37 37
38 38 def _toolstr(ui, tool, part, *args):
39 39 return ui.config("merge-tools", tool + "." + part, *args)
40 40
41 41 def _toolbool(ui, tool, part,*args):
42 42 return ui.configbool("merge-tools", tool + "." + part, *args)
43 43
44 44 def _toollist(ui, tool, part):
45 45 return ui.configlist("merge-tools", tool + "." + part)
46 46
47 47 internals = {}
48 48 # Merge tools to document.
49 49 internalsdoc = {}
50 50
51 51 internaltool = registrar.internalmerge()
52 52
53 53 # internal tool merge types
54 54 nomerge = internaltool.nomerge
55 55 mergeonly = internaltool.mergeonly # just the full merge, no premerge
56 56 fullmerge = internaltool.fullmerge # both premerge and merge
57 57
58 58 _localchangedotherdeletedmsg = _(
59 59 "local%(l)s changed %(fd)s which other%(o)s deleted\n"
60 60 "use (c)hanged version, (d)elete, or leave (u)nresolved?"
61 61 "$$ &Changed $$ &Delete $$ &Unresolved")
62 62
63 63 _otherchangedlocaldeletedmsg = _(
64 64 "other%(o)s changed %(fd)s which local%(l)s deleted\n"
65 65 "use (c)hanged version, leave (d)eleted, or "
66 66 "leave (u)nresolved?"
67 67 "$$ &Changed $$ &Deleted $$ &Unresolved")
68 68
69 69 class absentfilectx(object):
70 70 """Represents a file that's ostensibly in a context but is actually not
71 71 present in it.
72 72
73 73 This is here because it's very specific to the filemerge code for now --
74 74 other code is likely going to break with the values this returns."""
75 75 def __init__(self, ctx, f):
76 76 self._ctx = ctx
77 77 self._f = f
78 78
79 79 def path(self):
80 80 return self._f
81 81
82 82 def size(self):
83 83 return None
84 84
85 85 def data(self):
86 86 return None
87 87
88 88 def filenode(self):
89 89 return nullid
90 90
91 91 _customcmp = True
92 92 def cmp(self, fctx):
93 93 """compare with other file context
94 94
95 95 returns True if different from fctx.
96 96 """
97 97 return not (fctx.isabsent() and
98 98 fctx.ctx() == self.ctx() and
99 99 fctx.path() == self.path())
100 100
101 101 def flags(self):
102 102 return ''
103 103
104 104 def changectx(self):
105 105 return self._ctx
106 106
107 107 def isbinary(self):
108 108 return False
109 109
110 110 def isabsent(self):
111 111 return True
112 112
113 113 def _findtool(ui, tool):
114 114 if tool in internals:
115 115 return tool
116 116 cmd = _toolstr(ui, tool, "executable", tool)
117 117 if cmd.startswith('python:'):
118 118 return cmd
119 119 return findexternaltool(ui, tool)
120 120
121 121 def _quotetoolpath(cmd):
122 122 if cmd.startswith('python:'):
123 123 return cmd
124 124 return procutil.shellquote(cmd)
125 125
126 126 def findexternaltool(ui, tool):
127 127 for kn in ("regkey", "regkeyalt"):
128 128 k = _toolstr(ui, tool, kn)
129 129 if not k:
130 130 continue
131 131 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
132 132 if p:
133 133 p = procutil.findexe(p + _toolstr(ui, tool, "regappend", ""))
134 134 if p:
135 135 return p
136 136 exe = _toolstr(ui, tool, "executable", tool)
137 137 return procutil.findexe(util.expandpath(exe))
138 138
139 139 def _picktool(repo, ui, path, binary, symlink, changedelete):
140 def hascapability(tool, capability, strict=False):
141 if strict and tool in internals:
142 if internals[tool].capabilities.get(capability):
143 return True
144 return _toolbool(ui, tool, capability)
145
140 146 def supportscd(tool):
141 147 return tool in internals and internals[tool].mergetype == nomerge
142 148
143 149 def check(tool, pat, symlink, binary, changedelete):
144 150 tmsg = tool
145 151 if pat:
146 152 tmsg = _("%s (for pattern %s)") % (tool, pat)
147 153 if not _findtool(ui, tool):
148 154 if pat: # explicitly requested tool deserves a warning
149 155 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
150 156 else: # configured but non-existing tools are more silent
151 157 ui.note(_("couldn't find merge tool %s\n") % tmsg)
152 elif symlink and not _toolbool(ui, tool, "symlink"):
158 elif symlink and not hascapability(tool, "symlink"):
153 159 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
154 elif binary and not _toolbool(ui, tool, "binary"):
160 elif binary and not hascapability(tool, "binary"):
155 161 ui.warn(_("tool %s can't handle binary\n") % tmsg)
156 162 elif changedelete and not supportscd(tool):
157 163 # the nomerge tools are the only tools that support change/delete
158 164 # conflicts
159 165 pass
160 166 elif not procutil.gui() and _toolbool(ui, tool, "gui"):
161 167 ui.warn(_("tool %s requires a GUI\n") % tmsg)
162 168 else:
163 169 return True
164 170 return False
165 171
166 172 # internal config: ui.forcemerge
167 173 # forcemerge comes from command line arguments, highest priority
168 174 force = ui.config('ui', 'forcemerge')
169 175 if force:
170 176 toolpath = _findtool(ui, force)
171 177 if changedelete and not supportscd(toolpath):
172 178 return ":prompt", None
173 179 else:
174 180 if toolpath:
175 181 return (force, _quotetoolpath(toolpath))
176 182 else:
177 183 # mimic HGMERGE if given tool not found
178 184 return (force, force)
179 185
180 186 # HGMERGE takes next precedence
181 187 hgmerge = encoding.environ.get("HGMERGE")
182 188 if hgmerge:
183 189 if changedelete and not supportscd(hgmerge):
184 190 return ":prompt", None
185 191 else:
186 192 return (hgmerge, hgmerge)
187 193
188 194 # then patterns
189 195 for pat, tool in ui.configitems("merge-patterns"):
190 196 mf = match.match(repo.root, '', [pat])
191 197 if mf(path) and check(tool, pat, symlink, False, changedelete):
192 198 toolpath = _findtool(ui, tool)
193 199 return (tool, _quotetoolpath(toolpath))
194 200
195 201 # then merge tools
196 202 tools = {}
197 203 disabled = set()
198 204 for k, v in ui.configitems("merge-tools"):
199 205 t = k.split('.')[0]
200 206 if t not in tools:
201 207 tools[t] = int(_toolstr(ui, t, "priority"))
202 208 if _toolbool(ui, t, "disabled"):
203 209 disabled.add(t)
204 210 names = tools.keys()
205 211 tools = sorted([(-p, tool) for tool, p in tools.items()
206 212 if tool not in disabled])
207 213 uimerge = ui.config("ui", "merge")
208 214 if uimerge:
209 215 # external tools defined in uimerge won't be able to handle
210 216 # change/delete conflicts
211 217 if check(uimerge, path, symlink, binary, changedelete):
212 218 if uimerge not in names and not changedelete:
213 219 return (uimerge, uimerge)
214 220 tools.insert(0, (None, uimerge)) # highest priority
215 221 tools.append((None, "hgmerge")) # the old default, if found
216 222 for p, t in tools:
217 223 if check(t, None, symlink, binary, changedelete):
218 224 toolpath = _findtool(ui, t)
219 225 return (t, _quotetoolpath(toolpath))
220 226
221 227 # internal merge or prompt as last resort
222 228 if symlink or binary or changedelete:
223 229 if not changedelete and len(tools):
224 230 # any tool is rejected by capability for symlink or binary
225 231 ui.warn(_("no tool found to merge %s\n") % path)
226 232 return ":prompt", None
227 233 return ":merge", None
228 234
229 235 def _eoltype(data):
230 236 "Guess the EOL type of a file"
231 237 if '\0' in data: # binary
232 238 return None
233 239 if '\r\n' in data: # Windows
234 240 return '\r\n'
235 241 if '\r' in data: # Old Mac
236 242 return '\r'
237 243 if '\n' in data: # UNIX
238 244 return '\n'
239 245 return None # unknown
240 246
241 247 def _matcheol(file, back):
242 248 "Convert EOL markers in a file to match origfile"
243 249 tostyle = _eoltype(back.data()) # No repo.wread filters?
244 250 if tostyle:
245 251 data = util.readfile(file)
246 252 style = _eoltype(data)
247 253 if style:
248 254 newdata = data.replace(style, tostyle)
249 255 if newdata != data:
250 256 util.writefile(file, newdata)
251 257
252 258 @internaltool('prompt', nomerge)
253 259 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
254 260 """Asks the user which of the local `p1()` or the other `p2()` version to
255 261 keep as the merged version."""
256 262 ui = repo.ui
257 263 fd = fcd.path()
258 264
259 265 # Avoid prompting during an in-memory merge since it doesn't support merge
260 266 # conflicts.
261 267 if fcd.changectx().isinmemory():
262 268 raise error.InMemoryMergeConflictsError('in-memory merge does not '
263 269 'support file conflicts')
264 270
265 271 prompts = partextras(labels)
266 272 prompts['fd'] = fd
267 273 try:
268 274 if fco.isabsent():
269 275 index = ui.promptchoice(
270 276 _localchangedotherdeletedmsg % prompts, 2)
271 277 choice = ['local', 'other', 'unresolved'][index]
272 278 elif fcd.isabsent():
273 279 index = ui.promptchoice(
274 280 _otherchangedlocaldeletedmsg % prompts, 2)
275 281 choice = ['other', 'local', 'unresolved'][index]
276 282 else:
277 283 index = ui.promptchoice(
278 284 _("keep (l)ocal%(l)s, take (o)ther%(o)s, or leave (u)nresolved"
279 285 " for %(fd)s?"
280 286 "$$ &Local $$ &Other $$ &Unresolved") % prompts, 2)
281 287 choice = ['local', 'other', 'unresolved'][index]
282 288
283 289 if choice == 'other':
284 290 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf,
285 291 labels)
286 292 elif choice == 'local':
287 293 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf,
288 294 labels)
289 295 elif choice == 'unresolved':
290 296 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
291 297 labels)
292 298 except error.ResponseExpected:
293 299 ui.write("\n")
294 300 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
295 301 labels)
296 302
297 303 @internaltool('local', nomerge)
298 304 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
299 305 """Uses the local `p1()` version of files as the merged version."""
300 306 return 0, fcd.isabsent()
301 307
302 308 @internaltool('other', nomerge)
303 309 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
304 310 """Uses the other `p2()` version of files as the merged version."""
305 311 if fco.isabsent():
306 312 # local changed, remote deleted -- 'deleted' picked
307 313 _underlyingfctxifabsent(fcd).remove()
308 314 deleted = True
309 315 else:
310 316 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
311 317 deleted = False
312 318 return 0, deleted
313 319
314 320 @internaltool('fail', nomerge)
315 321 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
316 322 """
317 323 Rather than attempting to merge files that were modified on both
318 324 branches, it marks them as unresolved. The resolve command must be
319 325 used to resolve these conflicts."""
320 326 # for change/delete conflicts write out the changed version, then fail
321 327 if fcd.isabsent():
322 328 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
323 329 return 1, False
324 330
325 331 def _underlyingfctxifabsent(filectx):
326 332 """Sometimes when resolving, our fcd is actually an absentfilectx, but
327 333 we want to write to it (to do the resolve). This helper returns the
328 334 underyling workingfilectx in that case.
329 335 """
330 336 if filectx.isabsent():
331 337 return filectx.changectx()[filectx.path()]
332 338 else:
333 339 return filectx
334 340
335 341 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
336 342 tool, toolpath, binary, symlink, scriptfn = toolconf
337 343 if symlink or fcd.isabsent() or fco.isabsent():
338 344 return 1
339 345 unused, unused, unused, back = files
340 346
341 347 ui = repo.ui
342 348
343 349 validkeep = ['keep', 'keep-merge3']
344 350
345 351 # do we attempt to simplemerge first?
346 352 try:
347 353 premerge = _toolbool(ui, tool, "premerge", not binary)
348 354 except error.ConfigError:
349 355 premerge = _toolstr(ui, tool, "premerge", "").lower()
350 356 if premerge not in validkeep:
351 357 _valid = ', '.join(["'" + v + "'" for v in validkeep])
352 358 raise error.ConfigError(_("%s.premerge not valid "
353 359 "('%s' is neither boolean nor %s)") %
354 360 (tool, premerge, _valid))
355 361
356 362 if premerge:
357 363 if premerge == 'keep-merge3':
358 364 if not labels:
359 365 labels = _defaultconflictlabels
360 366 if len(labels) < 3:
361 367 labels.append('base')
362 368 r = simplemerge.simplemerge(ui, fcd, fca, fco, quiet=True, label=labels)
363 369 if not r:
364 370 ui.debug(" premerge successful\n")
365 371 return 0
366 372 if premerge not in validkeep:
367 373 # restore from backup and try again
368 374 _restorebackup(fcd, back)
369 375 return 1 # continue merging
370 376
371 377 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
372 378 tool, toolpath, binary, symlink, scriptfn = toolconf
373 379 if symlink:
374 380 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
375 381 'for %s\n') % (tool, fcd.path()))
376 382 return False
377 383 if fcd.isabsent() or fco.isabsent():
378 384 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
379 385 'conflict for %s\n') % (tool, fcd.path()))
380 386 return False
381 387 return True
382 388
383 389 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
384 390 """
385 391 Uses the internal non-interactive simple merge algorithm for merging
386 392 files. It will fail if there are any conflicts and leave markers in
387 393 the partially merged file. Markers will have two sections, one for each side
388 394 of merge, unless mode equals 'union' which suppresses the markers."""
389 395 ui = repo.ui
390 396
391 397 r = simplemerge.simplemerge(ui, fcd, fca, fco, label=labels, mode=mode)
392 398 return True, r, False
393 399
394 400 @internaltool('union', fullmerge,
395 401 _("warning: conflicts while merging %s! "
396 402 "(edit, then use 'hg resolve --mark')\n"),
397 403 precheck=_mergecheck)
398 404 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
399 405 """
400 406 Uses the internal non-interactive simple merge algorithm for merging
401 407 files. It will use both left and right sides for conflict regions.
402 408 No markers are inserted."""
403 409 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
404 410 files, labels, 'union')
405 411
406 412 @internaltool('merge', fullmerge,
407 413 _("warning: conflicts while merging %s! "
408 414 "(edit, then use 'hg resolve --mark')\n"),
409 415 precheck=_mergecheck)
410 416 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
411 417 """
412 418 Uses the internal non-interactive simple merge algorithm for merging
413 419 files. It will fail if there are any conflicts and leave markers in
414 420 the partially merged file. Markers will have two sections, one for each side
415 421 of merge."""
416 422 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
417 423 files, labels, 'merge')
418 424
419 425 @internaltool('merge3', fullmerge,
420 426 _("warning: conflicts while merging %s! "
421 427 "(edit, then use 'hg resolve --mark')\n"),
422 428 precheck=_mergecheck)
423 429 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
424 430 """
425 431 Uses the internal non-interactive simple merge algorithm for merging
426 432 files. It will fail if there are any conflicts and leave markers in
427 433 the partially merged file. Marker will have three sections, one from each
428 434 side of the merge and one for the base content."""
429 435 if not labels:
430 436 labels = _defaultconflictlabels
431 437 if len(labels) < 3:
432 438 labels.append('base')
433 439 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
434 440
435 441 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
436 442 labels=None, localorother=None):
437 443 """
438 444 Generic driver for _imergelocal and _imergeother
439 445 """
440 446 assert localorother is not None
441 447 tool, toolpath, binary, symlink, scriptfn = toolconf
442 448 r = simplemerge.simplemerge(repo.ui, fcd, fca, fco, label=labels,
443 449 localorother=localorother)
444 450 return True, r
445 451
446 452 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
447 453 def _imergelocal(*args, **kwargs):
448 454 """
449 455 Like :merge, but resolve all conflicts non-interactively in favor
450 456 of the local `p1()` changes."""
451 457 success, status = _imergeauto(localorother='local', *args, **kwargs)
452 458 return success, status, False
453 459
454 460 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
455 461 def _imergeother(*args, **kwargs):
456 462 """
457 463 Like :merge, but resolve all conflicts non-interactively in favor
458 464 of the other `p2()` changes."""
459 465 success, status = _imergeauto(localorother='other', *args, **kwargs)
460 466 return success, status, False
461 467
462 468 @internaltool('tagmerge', mergeonly,
463 469 _("automatic tag merging of %s failed! "
464 470 "(use 'hg resolve --tool :merge' or another merge "
465 471 "tool of your choice)\n"))
466 472 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
467 473 """
468 474 Uses the internal tag merge algorithm (experimental).
469 475 """
470 476 success, status = tagmerge.merge(repo, fcd, fco, fca)
471 477 return success, status, False
472 478
473 479 @internaltool('dump', fullmerge, binary=True, symlink=True)
474 480 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
475 481 """
476 482 Creates three versions of the files to merge, containing the
477 483 contents of local, other and base. These files can then be used to
478 484 perform a merge manually. If the file to be merged is named
479 485 ``a.txt``, these files will accordingly be named ``a.txt.local``,
480 486 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
481 487 same directory as ``a.txt``.
482 488
483 489 This implies premerge. Therefore, files aren't dumped, if premerge
484 490 runs successfully. Use :forcedump to forcibly write files out.
485 491 """
486 492 a = _workingpath(repo, fcd)
487 493 fd = fcd.path()
488 494
489 495 from . import context
490 496 if isinstance(fcd, context.overlayworkingfilectx):
491 497 raise error.InMemoryMergeConflictsError('in-memory merge does not '
492 498 'support the :dump tool.')
493 499
494 500 util.writefile(a + ".local", fcd.decodeddata())
495 501 repo.wwrite(fd + ".other", fco.data(), fco.flags())
496 502 repo.wwrite(fd + ".base", fca.data(), fca.flags())
497 503 return False, 1, False
498 504
499 505 @internaltool('forcedump', mergeonly, binary=True, symlink=True)
500 506 def _forcedump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
501 507 labels=None):
502 508 """
503 509 Creates three versions of the files as same as :dump, but omits premerge.
504 510 """
505 511 return _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
506 512 labels=labels)
507 513
508 514 def _xmergeimm(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
509 515 # In-memory merge simply raises an exception on all external merge tools,
510 516 # for now.
511 517 #
512 518 # It would be possible to run most tools with temporary files, but this
513 519 # raises the question of what to do if the user only partially resolves the
514 520 # file -- we can't leave a merge state. (Copy to somewhere in the .hg/
515 521 # directory and tell the user how to get it is my best idea, but it's
516 522 # clunky.)
517 523 raise error.InMemoryMergeConflictsError('in-memory merge does not support '
518 524 'external merge tools')
519 525
520 526 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
521 527 tool, toolpath, binary, symlink, scriptfn = toolconf
522 528 if fcd.isabsent() or fco.isabsent():
523 529 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
524 530 'for %s\n') % (tool, fcd.path()))
525 531 return False, 1, None
526 532 unused, unused, unused, back = files
527 533 localpath = _workingpath(repo, fcd)
528 534 args = _toolstr(repo.ui, tool, "args")
529 535
530 536 with _maketempfiles(repo, fco, fca, repo.wvfs.join(back.path()),
531 537 "$output" in args) as temppaths:
532 538 basepath, otherpath, localoutputpath = temppaths
533 539 outpath = ""
534 540 mylabel, otherlabel = labels[:2]
535 541 if len(labels) >= 3:
536 542 baselabel = labels[2]
537 543 else:
538 544 baselabel = 'base'
539 545 env = {'HG_FILE': fcd.path(),
540 546 'HG_MY_NODE': short(mynode),
541 547 'HG_OTHER_NODE': short(fco.changectx().node()),
542 548 'HG_BASE_NODE': short(fca.changectx().node()),
543 549 'HG_MY_ISLINK': 'l' in fcd.flags(),
544 550 'HG_OTHER_ISLINK': 'l' in fco.flags(),
545 551 'HG_BASE_ISLINK': 'l' in fca.flags(),
546 552 'HG_MY_LABEL': mylabel,
547 553 'HG_OTHER_LABEL': otherlabel,
548 554 'HG_BASE_LABEL': baselabel,
549 555 }
550 556 ui = repo.ui
551 557
552 558 if "$output" in args:
553 559 # read input from backup, write to original
554 560 outpath = localpath
555 561 localpath = localoutputpath
556 562 replace = {'local': localpath, 'base': basepath, 'other': otherpath,
557 563 'output': outpath, 'labellocal': mylabel,
558 564 'labelother': otherlabel, 'labelbase': baselabel}
559 565 args = util.interpolate(
560 566 br'\$', replace, args,
561 567 lambda s: procutil.shellquote(util.localpath(s)))
562 568 if _toolbool(ui, tool, "gui"):
563 569 repo.ui.status(_('running merge tool %s for file %s\n') %
564 570 (tool, fcd.path()))
565 571 if scriptfn is None:
566 572 cmd = toolpath + ' ' + args
567 573 repo.ui.debug('launching merge tool: %s\n' % cmd)
568 574 r = ui.system(cmd, cwd=repo.root, environ=env,
569 575 blockedtag='mergetool')
570 576 else:
571 577 repo.ui.debug('launching python merge script: %s:%s\n' %
572 578 (toolpath, scriptfn))
573 579 r = 0
574 580 try:
575 581 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
576 582 from . import extensions
577 583 mod = extensions.loadpath(toolpath, 'hgmerge.%s' % tool)
578 584 except Exception:
579 585 raise error.Abort(_("loading python merge script failed: %s") %
580 586 toolpath)
581 587 mergefn = getattr(mod, scriptfn, None)
582 588 if mergefn is None:
583 589 raise error.Abort(_("%s does not have function: %s") %
584 590 (toolpath, scriptfn))
585 591 argslist = procutil.shellsplit(args)
586 592 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
587 593 from . import hook
588 594 ret, raised = hook.pythonhook(ui, repo, "merge", toolpath,
589 595 mergefn, {'args': argslist}, True)
590 596 if raised:
591 597 r = 1
592 598 repo.ui.debug('merge tool returned: %d\n' % r)
593 599 return True, r, False
594 600
595 601 def _formatconflictmarker(ctx, template, label, pad):
596 602 """Applies the given template to the ctx, prefixed by the label.
597 603
598 604 Pad is the minimum width of the label prefix, so that multiple markers
599 605 can have aligned templated parts.
600 606 """
601 607 if ctx.node() is None:
602 608 ctx = ctx.p1()
603 609
604 610 props = {'ctx': ctx}
605 611 templateresult = template.renderdefault(props)
606 612
607 613 label = ('%s:' % label).ljust(pad + 1)
608 614 mark = '%s %s' % (label, templateresult)
609 615
610 616 if mark:
611 617 mark = mark.splitlines()[0] # split for safety
612 618
613 619 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
614 620 return stringutil.ellipsis(mark, 80 - 8)
615 621
616 622 _defaultconflictlabels = ['local', 'other']
617 623
618 624 def _formatlabels(repo, fcd, fco, fca, labels, tool=None):
619 625 """Formats the given labels using the conflict marker template.
620 626
621 627 Returns a list of formatted labels.
622 628 """
623 629 cd = fcd.changectx()
624 630 co = fco.changectx()
625 631 ca = fca.changectx()
626 632
627 633 ui = repo.ui
628 634 template = ui.config('ui', 'mergemarkertemplate')
629 635 if tool is not None:
630 636 template = _toolstr(ui, tool, 'mergemarkertemplate', template)
631 637 template = templater.unquotestring(template)
632 638 tres = formatter.templateresources(ui, repo)
633 639 tmpl = formatter.maketemplater(ui, template, defaults=templatekw.keywords,
634 640 resources=tres)
635 641
636 642 pad = max(len(l) for l in labels)
637 643
638 644 newlabels = [_formatconflictmarker(cd, tmpl, labels[0], pad),
639 645 _formatconflictmarker(co, tmpl, labels[1], pad)]
640 646 if len(labels) > 2:
641 647 newlabels.append(_formatconflictmarker(ca, tmpl, labels[2], pad))
642 648 return newlabels
643 649
644 650 def partextras(labels):
645 651 """Return a dictionary of extra labels for use in prompts to the user
646 652
647 653 Intended use is in strings of the form "(l)ocal%(l)s".
648 654 """
649 655 if labels is None:
650 656 return {
651 657 "l": "",
652 658 "o": "",
653 659 }
654 660
655 661 return {
656 662 "l": " [%s]" % labels[0],
657 663 "o": " [%s]" % labels[1],
658 664 }
659 665
660 666 def _restorebackup(fcd, back):
661 667 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
662 668 # util.copy here instead.
663 669 fcd.write(back.data(), fcd.flags())
664 670
665 671 def _makebackup(repo, ui, wctx, fcd, premerge):
666 672 """Makes and returns a filectx-like object for ``fcd``'s backup file.
667 673
668 674 In addition to preserving the user's pre-existing modifications to `fcd`
669 675 (if any), the backup is used to undo certain premerges, confirm whether a
670 676 merge changed anything, and determine what line endings the new file should
671 677 have.
672 678
673 679 Backups only need to be written once (right before the premerge) since their
674 680 content doesn't change afterwards.
675 681 """
676 682 if fcd.isabsent():
677 683 return None
678 684 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
679 685 # merge -> filemerge). (I suspect the fileset import is the weakest link)
680 686 from . import context
681 687 a = _workingpath(repo, fcd)
682 688 back = scmutil.origpath(ui, repo, a)
683 689 inworkingdir = (back.startswith(repo.wvfs.base) and not
684 690 back.startswith(repo.vfs.base))
685 691 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
686 692 # If the backup file is to be in the working directory, and we're
687 693 # merging in-memory, we must redirect the backup to the memory context
688 694 # so we don't disturb the working directory.
689 695 relpath = back[len(repo.wvfs.base) + 1:]
690 696 if premerge:
691 697 wctx[relpath].write(fcd.data(), fcd.flags())
692 698 return wctx[relpath]
693 699 else:
694 700 if premerge:
695 701 # Otherwise, write to wherever path the user specified the backups
696 702 # should go. We still need to switch based on whether the source is
697 703 # in-memory so we can use the fast path of ``util.copy`` if both are
698 704 # on disk.
699 705 if isinstance(fcd, context.overlayworkingfilectx):
700 706 util.writefile(back, fcd.data())
701 707 else:
702 708 util.copyfile(a, back)
703 709 # A arbitraryfilectx is returned, so we can run the same functions on
704 710 # the backup context regardless of where it lives.
705 711 return context.arbitraryfilectx(back, repo=repo)
706 712
707 713 @contextlib.contextmanager
708 714 def _maketempfiles(repo, fco, fca, localpath, uselocalpath):
709 715 """Writes out `fco` and `fca` as temporary files, and (if uselocalpath)
710 716 copies `localpath` to another temporary file, so an external merge tool may
711 717 use them.
712 718 """
713 719 tmproot = None
714 720 tmprootprefix = repo.ui.config('experimental', 'mergetempdirprefix')
715 721 if tmprootprefix:
716 722 tmproot = pycompat.mkdtemp(prefix=tmprootprefix)
717 723
718 724 def maketempfrompath(prefix, path):
719 725 fullbase, ext = os.path.splitext(path)
720 726 pre = "%s~%s" % (os.path.basename(fullbase), prefix)
721 727 if tmproot:
722 728 name = os.path.join(tmproot, pre)
723 729 if ext:
724 730 name += ext
725 731 f = open(name, r"wb")
726 732 else:
727 733 fd, name = pycompat.mkstemp(prefix=pre + '.', suffix=ext)
728 734 f = os.fdopen(fd, r"wb")
729 735 return f, name
730 736
731 737 def tempfromcontext(prefix, ctx):
732 738 f, name = maketempfrompath(prefix, ctx.path())
733 739 data = repo.wwritedata(ctx.path(), ctx.data())
734 740 f.write(data)
735 741 f.close()
736 742 return name
737 743
738 744 b = tempfromcontext("base", fca)
739 745 c = tempfromcontext("other", fco)
740 746 d = localpath
741 747 if uselocalpath:
742 748 # We start off with this being the backup filename, so remove the .orig
743 749 # to make syntax-highlighting more likely.
744 750 if d.endswith('.orig'):
745 751 d, _ = os.path.splitext(d)
746 752 f, d = maketempfrompath("local", d)
747 753 with open(localpath, 'rb') as src:
748 754 f.write(src.read())
749 755 f.close()
750 756
751 757 try:
752 758 yield b, c, d
753 759 finally:
754 760 if tmproot:
755 761 shutil.rmtree(tmproot)
756 762 else:
757 763 util.unlink(b)
758 764 util.unlink(c)
759 765 # if not uselocalpath, d is the 'orig'/backup file which we
760 766 # shouldn't delete.
761 767 if d and uselocalpath:
762 768 util.unlink(d)
763 769
764 770 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
765 771 """perform a 3-way merge in the working directory
766 772
767 773 premerge = whether this is a premerge
768 774 mynode = parent node before merge
769 775 orig = original local filename before merge
770 776 fco = other file context
771 777 fca = ancestor file context
772 778 fcd = local file context for current/destination file
773 779
774 780 Returns whether the merge is complete, the return value of the merge, and
775 781 a boolean indicating whether the file was deleted from disk."""
776 782
777 783 if not fco.cmp(fcd): # files identical?
778 784 return True, None, False
779 785
780 786 ui = repo.ui
781 787 fd = fcd.path()
782 788 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
783 789 symlink = 'l' in fcd.flags() + fco.flags()
784 790 changedelete = fcd.isabsent() or fco.isabsent()
785 791 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
786 792 scriptfn = None
787 793 if tool in internals and tool.startswith('internal:'):
788 794 # normalize to new-style names (':merge' etc)
789 795 tool = tool[len('internal'):]
790 796 if toolpath and toolpath.startswith('python:'):
791 797 invalidsyntax = False
792 798 if toolpath.count(':') >= 2:
793 799 script, scriptfn = toolpath[7:].rsplit(':', 1)
794 800 if not scriptfn:
795 801 invalidsyntax = True
796 802 # missing :callable can lead to spliting on windows drive letter
797 803 if '\\' in scriptfn or '/' in scriptfn:
798 804 invalidsyntax = True
799 805 else:
800 806 invalidsyntax = True
801 807 if invalidsyntax:
802 808 raise error.Abort(_("invalid 'python:' syntax: %s") % toolpath)
803 809 toolpath = script
804 810 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
805 811 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
806 812 pycompat.bytestr(changedelete)))
807 813
808 814 if tool in internals:
809 815 func = internals[tool]
810 816 mergetype = func.mergetype
811 817 onfailure = func.onfailure
812 818 precheck = func.precheck
813 819 isexternal = False
814 820 else:
815 821 if wctx.isinmemory():
816 822 func = _xmergeimm
817 823 else:
818 824 func = _xmerge
819 825 mergetype = fullmerge
820 826 onfailure = _("merging %s failed!\n")
821 827 precheck = None
822 828 isexternal = True
823 829
824 830 toolconf = tool, toolpath, binary, symlink, scriptfn
825 831
826 832 if mergetype == nomerge:
827 833 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
828 834 return True, r, deleted
829 835
830 836 if premerge:
831 837 if orig != fco.path():
832 838 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
833 839 else:
834 840 ui.status(_("merging %s\n") % fd)
835 841
836 842 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
837 843
838 844 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
839 845 toolconf):
840 846 if onfailure:
841 847 if wctx.isinmemory():
842 848 raise error.InMemoryMergeConflictsError('in-memory merge does '
843 849 'not support merge '
844 850 'conflicts')
845 851 ui.warn(onfailure % fd)
846 852 return True, 1, False
847 853
848 854 back = _makebackup(repo, ui, wctx, fcd, premerge)
849 855 files = (None, None, None, back)
850 856 r = 1
851 857 try:
852 858 internalmarkerstyle = ui.config('ui', 'mergemarkers')
853 859 if isexternal:
854 860 markerstyle = _toolstr(ui, tool, 'mergemarkers')
855 861 else:
856 862 markerstyle = internalmarkerstyle
857 863
858 864 if not labels:
859 865 labels = _defaultconflictlabels
860 866 formattedlabels = labels
861 867 if markerstyle != 'basic':
862 868 formattedlabels = _formatlabels(repo, fcd, fco, fca, labels,
863 869 tool=tool)
864 870
865 871 if premerge and mergetype == fullmerge:
866 872 # conflict markers generated by premerge will use 'detailed'
867 873 # settings if either ui.mergemarkers or the tool's mergemarkers
868 874 # setting is 'detailed'. This way tools can have basic labels in
869 875 # space-constrained areas of the UI, but still get full information
870 876 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
871 877 premergelabels = labels
872 878 labeltool = None
873 879 if markerstyle != 'basic':
874 880 # respect 'tool's mergemarkertemplate (which defaults to
875 881 # ui.mergemarkertemplate)
876 882 labeltool = tool
877 883 if internalmarkerstyle != 'basic' or markerstyle != 'basic':
878 884 premergelabels = _formatlabels(repo, fcd, fco, fca,
879 885 premergelabels, tool=labeltool)
880 886
881 887 r = _premerge(repo, fcd, fco, fca, toolconf, files,
882 888 labels=premergelabels)
883 889 # complete if premerge successful (r is 0)
884 890 return not r, r, False
885 891
886 892 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
887 893 toolconf, files, labels=formattedlabels)
888 894
889 895 if needcheck:
890 896 r = _check(repo, r, ui, tool, fcd, files)
891 897
892 898 if r:
893 899 if onfailure:
894 900 if wctx.isinmemory():
895 901 raise error.InMemoryMergeConflictsError('in-memory merge '
896 902 'does not support '
897 903 'merge conflicts')
898 904 ui.warn(onfailure % fd)
899 905 _onfilemergefailure(ui)
900 906
901 907 return True, r, deleted
902 908 finally:
903 909 if not r and back is not None:
904 910 back.remove()
905 911
906 912 def _haltmerge():
907 913 msg = _('merge halted after failed merge (see hg resolve)')
908 914 raise error.InterventionRequired(msg)
909 915
910 916 def _onfilemergefailure(ui):
911 917 action = ui.config('merge', 'on-failure')
912 918 if action == 'prompt':
913 919 msg = _('continue merge operation (yn)?' '$$ &Yes $$ &No')
914 920 if ui.promptchoice(msg, 0) == 1:
915 921 _haltmerge()
916 922 if action == 'halt':
917 923 _haltmerge()
918 924 # default action is 'continue', in which case we neither prompt nor halt
919 925
920 926 def hasconflictmarkers(data):
921 927 return bool(re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", data,
922 928 re.MULTILINE))
923 929
924 930 def _check(repo, r, ui, tool, fcd, files):
925 931 fd = fcd.path()
926 932 unused, unused, unused, back = files
927 933
928 934 if not r and (_toolbool(ui, tool, "checkconflicts") or
929 935 'conflicts' in _toollist(ui, tool, "check")):
930 936 if hasconflictmarkers(fcd.data()):
931 937 r = 1
932 938
933 939 checked = False
934 940 if 'prompt' in _toollist(ui, tool, "check"):
935 941 checked = True
936 942 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
937 943 "$$ &Yes $$ &No") % fd, 1):
938 944 r = 1
939 945
940 946 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
941 947 'changed' in
942 948 _toollist(ui, tool, "check")):
943 949 if back is not None and not fcd.cmp(back):
944 950 if ui.promptchoice(_(" output file %s appears unchanged\n"
945 951 "was merge successful (yn)?"
946 952 "$$ &Yes $$ &No") % fd, 1):
947 953 r = 1
948 954
949 955 if back is not None and _toolbool(ui, tool, "fixeol"):
950 956 _matcheol(_workingpath(repo, fcd), back)
951 957
952 958 return r
953 959
954 960 def _workingpath(repo, ctx):
955 961 return repo.wjoin(ctx.path())
956 962
957 963 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
958 964 return _filemerge(True, repo, wctx, mynode, orig, fcd, fco, fca,
959 965 labels=labels)
960 966
961 967 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
962 968 return _filemerge(False, repo, wctx, mynode, orig, fcd, fco, fca,
963 969 labels=labels)
964 970
965 971 def loadinternalmerge(ui, extname, registrarobj):
966 972 """Load internal merge tool from specified registrarobj
967 973 """
968 974 for name, func in registrarobj._table.iteritems():
969 975 fullname = ':' + name
970 976 internals[fullname] = func
971 977 internals['internal:' + name] = func
972 978 internalsdoc[fullname] = func
973 979
974 980 # load built-in merge tools explicitly to setup internalsdoc
975 981 loadinternalmerge(None, None, internaltool)
976 982
977 983 # tell hggettext to extract docstrings from these functions:
978 984 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now