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