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