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