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