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