##// END OF EJS Templates
filemerge: work with `simplemerge.MergeInput` in `filemerge()`...
Martin von Zweigbergk -
r49431:07069fcd default
parent child Browse files
Show More
@@ -1,1260 +1,1268
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, backup):
297 297 """Convert EOL markers in a file to match origfile"""
298 298 tostyle = _eoltype(backup.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, 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, fcd, fco, fca, toolconf, labels)
351 351 elif choice == b'local':
352 352 return _ilocal(repo, mynode, fcd, fco, fca, toolconf, labels)
353 353 elif choice == b'unresolved':
354 354 return _ifail(repo, mynode, fcd, fco, fca, toolconf, labels)
355 355 except error.ResponseExpected:
356 356 ui.write(b"\n")
357 357 return _ifail(repo, mynode, fcd, fco, fca, toolconf, labels)
358 358
359 359
360 360 @internaltool(b'local', nomerge)
361 361 def _ilocal(repo, mynode, 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, 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, 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, backup, labels):
403 403 tool, toolpath, binary, symlink, scriptfn = toolconf
404 404 if symlink or fcd.isabsent() or fco.isabsent():
405 405 return 1
406 406
407 407 ui = repo.ui
408 408
409 409 validkeep = [b'keep', b'keep-merge3', b'keep-mergediff']
410 410
411 411 # do we attempt to simplemerge first?
412 412 try:
413 413 premerge = _toolbool(ui, tool, b"premerge", not binary)
414 414 except error.ConfigError:
415 415 premerge = _toolstr(ui, tool, b"premerge", b"").lower()
416 416 if premerge not in validkeep:
417 417 _valid = b', '.join([b"'" + v + b"'" for v in validkeep])
418 418 raise error.ConfigError(
419 419 _(b"%s.premerge not valid ('%s' is neither boolean nor %s)")
420 420 % (tool, premerge, _valid)
421 421 )
422 422
423 423 if premerge:
424 424 mode = b'merge'
425 425 if premerge == b'keep-mergediff':
426 426 mode = b'mergediff'
427 427 elif premerge == b'keep-merge3':
428 428 mode = b'merge3'
429 429 local = simplemerge.MergeInput(fcd, labels[0])
430 430 other = simplemerge.MergeInput(fco, labels[1])
431 431 base = simplemerge.MergeInput(fca, labels[2])
432 432 r = simplemerge.simplemerge(
433 433 ui, local, base, other, quiet=True, mode=mode
434 434 )
435 435 if not r:
436 436 ui.debug(b" premerge successful\n")
437 437 return 0
438 438 if premerge not in validkeep:
439 439 # restore from backup and try again
440 440 _restorebackup(fcd, backup)
441 441 return 1 # continue merging
442 442
443 443
444 444 def _mergecheck(repo, mynode, fcd, fco, fca, toolconf):
445 445 tool, toolpath, binary, symlink, scriptfn = toolconf
446 446 uipathfn = scmutil.getuipathfn(repo)
447 447 if symlink:
448 448 repo.ui.warn(
449 449 _(b'warning: internal %s cannot merge symlinks for %s\n')
450 450 % (tool, uipathfn(fcd.path()))
451 451 )
452 452 return False
453 453 if fcd.isabsent() or fco.isabsent():
454 454 repo.ui.warn(
455 455 _(
456 456 b'warning: internal %s cannot merge change/delete '
457 457 b'conflict for %s\n'
458 458 )
459 459 % (tool, uipathfn(fcd.path()))
460 460 )
461 461 return False
462 462 return True
463 463
464 464
465 465 def _merge(repo, fcd, fco, fca, labels, mode):
466 466 """
467 467 Uses the internal non-interactive simple merge algorithm for merging
468 468 files. It will fail if there are any conflicts and leave markers in
469 469 the partially merged file. Markers will have two sections, one for each side
470 470 of merge, unless mode equals 'union' which suppresses the markers."""
471 471 ui = repo.ui
472 472
473 473 local = simplemerge.MergeInput(fcd)
474 474 local.label = labels[0]
475 475 other = simplemerge.MergeInput(fco)
476 476 other.label = labels[1]
477 477 base = simplemerge.MergeInput(fca)
478 478 base.label = labels[2]
479 479 r = simplemerge.simplemerge(ui, local, base, other, mode=mode)
480 480 return True, r, False
481 481
482 482
483 483 @internaltool(
484 484 b'union',
485 485 fullmerge,
486 486 _(
487 487 b"warning: conflicts while merging %s! "
488 488 b"(edit, then use 'hg resolve --mark')\n"
489 489 ),
490 490 precheck=_mergecheck,
491 491 )
492 492 def _iunion(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
493 493 """
494 494 Uses the internal non-interactive simple merge algorithm for merging
495 495 files. It will use both left and right sides for conflict regions.
496 496 No markers are inserted."""
497 497 return _merge(repo, fcd, fco, fca, labels, b'union')
498 498
499 499
500 500 @internaltool(
501 501 b'merge',
502 502 fullmerge,
503 503 _(
504 504 b"warning: conflicts while merging %s! "
505 505 b"(edit, then use 'hg resolve --mark')\n"
506 506 ),
507 507 precheck=_mergecheck,
508 508 )
509 509 def _imerge(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
510 510 """
511 511 Uses the internal non-interactive simple merge algorithm for merging
512 512 files. It will fail if there are any conflicts and leave markers in
513 513 the partially merged file. Markers will have two sections, one for each side
514 514 of merge."""
515 515 return _merge(repo, fcd, fco, fca, labels, b'merge')
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, fcd, fco, fca, toolconf, backup, 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 return _merge(repo, fcd, fco, fca, labels, b'merge3')
534 534
535 535
536 536 @internaltool(
537 537 b'merge3-lie-about-conflicts',
538 538 fullmerge,
539 539 b'',
540 540 precheck=_mergecheck,
541 541 )
542 542 def _imerge3alwaysgood(*args, **kwargs):
543 543 # Like merge3, but record conflicts as resolved with markers in place.
544 544 #
545 545 # This is used for `diff.merge` to show the differences between
546 546 # the auto-merge state and the committed merge state. It may be
547 547 # useful for other things.
548 548 b1, junk, b2 = _imerge3(*args, **kwargs)
549 549 # TODO is this right? I'm not sure what these return values mean,
550 550 # but as far as I can tell this will indicate to callers tha the
551 551 # merge succeeded.
552 552 return b1, False, b2
553 553
554 554
555 555 @internaltool(
556 556 b'mergediff',
557 557 fullmerge,
558 558 _(
559 559 b"warning: conflicts while merging %s! "
560 560 b"(edit, then use 'hg resolve --mark')\n"
561 561 ),
562 562 precheck=_mergecheck,
563 563 )
564 564 def _imerge_diff(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
565 565 """
566 566 Uses the internal non-interactive simple merge algorithm for merging
567 567 files. It will fail if there are any conflicts and leave markers in
568 568 the partially merged file. The marker will have two sections, one with the
569 569 content from one side of the merge, and one with a diff from the base
570 570 content to the content on the other side. (experimental)"""
571 571 return _merge(repo, fcd, fco, fca, labels, b'mergediff')
572 572
573 573
574 574 @internaltool(b'merge-local', mergeonly, precheck=_mergecheck)
575 575 def _imergelocal(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
576 576 """
577 577 Like :merge, but resolve all conflicts non-interactively in favor
578 578 of the local `p1()` changes."""
579 579 return _merge(repo, fcd, fco, fca, labels, b'local')
580 580
581 581
582 582 @internaltool(b'merge-other', mergeonly, precheck=_mergecheck)
583 583 def _imergeother(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
584 584 """
585 585 Like :merge, but resolve all conflicts non-interactively in favor
586 586 of the other `p2()` changes."""
587 587 return _merge(repo, fcd, fco, fca, labels, b'other')
588 588
589 589
590 590 @internaltool(
591 591 b'tagmerge',
592 592 mergeonly,
593 593 _(
594 594 b"automatic tag merging of %s failed! "
595 595 b"(use 'hg resolve --tool :merge' or another merge "
596 596 b"tool of your choice)\n"
597 597 ),
598 598 )
599 599 def _itagmerge(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
600 600 """
601 601 Uses the internal tag merge algorithm (experimental).
602 602 """
603 603 success, status = tagmerge.merge(repo, fcd, fco, fca)
604 604 return success, status, False
605 605
606 606
607 607 @internaltool(b'dump', fullmerge, binary=True, symlink=True)
608 608 def _idump(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
609 609 """
610 610 Creates three versions of the files to merge, containing the
611 611 contents of local, other and base. These files can then be used to
612 612 perform a merge manually. If the file to be merged is named
613 613 ``a.txt``, these files will accordingly be named ``a.txt.local``,
614 614 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
615 615 same directory as ``a.txt``.
616 616
617 617 This implies premerge. Therefore, files aren't dumped, if premerge
618 618 runs successfully. Use :forcedump to forcibly write files out.
619 619 """
620 620 a = _workingpath(repo, fcd)
621 621 fd = fcd.path()
622 622
623 623 from . import context
624 624
625 625 if isinstance(fcd, context.overlayworkingfilectx):
626 626 raise error.InMemoryMergeConflictsError(
627 627 b'in-memory merge does not support the :dump tool.'
628 628 )
629 629
630 630 util.writefile(a + b".local", fcd.decodeddata())
631 631 repo.wwrite(fd + b".other", fco.data(), fco.flags())
632 632 repo.wwrite(fd + b".base", fca.data(), fca.flags())
633 633 return False, 1, False
634 634
635 635
636 636 @internaltool(b'forcedump', mergeonly, binary=True, symlink=True)
637 637 def _forcedump(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
638 638 """
639 639 Creates three versions of the files as same as :dump, but omits premerge.
640 640 """
641 641 return _idump(repo, mynode, fcd, fco, fca, toolconf, backup, labels=labels)
642 642
643 643
644 644 def _xmergeimm(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
645 645 # In-memory merge simply raises an exception on all external merge tools,
646 646 # for now.
647 647 #
648 648 # It would be possible to run most tools with temporary files, but this
649 649 # raises the question of what to do if the user only partially resolves the
650 650 # file -- we can't leave a merge state. (Copy to somewhere in the .hg/
651 651 # directory and tell the user how to get it is my best idea, but it's
652 652 # clunky.)
653 653 raise error.InMemoryMergeConflictsError(
654 654 b'in-memory merge does not support external merge tools'
655 655 )
656 656
657 657
658 658 def _describemerge(ui, repo, mynode, fcl, fcb, fco, env, toolpath, args):
659 659 tmpl = ui.config(b'command-templates', b'pre-merge-tool-output')
660 660 if not tmpl:
661 661 return
662 662
663 663 mappingdict = templateutil.mappingdict
664 664 props = {
665 665 b'ctx': fcl.changectx(),
666 666 b'node': hex(mynode),
667 667 b'path': fcl.path(),
668 668 b'local': mappingdict(
669 669 {
670 670 b'ctx': fcl.changectx(),
671 671 b'fctx': fcl,
672 672 b'node': hex(mynode),
673 673 b'name': _(b'local'),
674 674 b'islink': b'l' in fcl.flags(),
675 675 b'label': env[b'HG_MY_LABEL'],
676 676 }
677 677 ),
678 678 b'base': mappingdict(
679 679 {
680 680 b'ctx': fcb.changectx(),
681 681 b'fctx': fcb,
682 682 b'name': _(b'base'),
683 683 b'islink': b'l' in fcb.flags(),
684 684 b'label': env[b'HG_BASE_LABEL'],
685 685 }
686 686 ),
687 687 b'other': mappingdict(
688 688 {
689 689 b'ctx': fco.changectx(),
690 690 b'fctx': fco,
691 691 b'name': _(b'other'),
692 692 b'islink': b'l' in fco.flags(),
693 693 b'label': env[b'HG_OTHER_LABEL'],
694 694 }
695 695 ),
696 696 b'toolpath': toolpath,
697 697 b'toolargs': args,
698 698 }
699 699
700 700 # TODO: make all of this something that can be specified on a per-tool basis
701 701 tmpl = templater.unquotestring(tmpl)
702 702
703 703 # Not using cmdutil.rendertemplate here since it causes errors importing
704 704 # things for us to import cmdutil.
705 705 tres = formatter.templateresources(ui, repo)
706 706 t = formatter.maketemplater(
707 707 ui, tmpl, defaults=templatekw.keywords, resources=tres
708 708 )
709 709 ui.status(t.renderdefault(props))
710 710
711 711
712 712 def _xmerge(repo, mynode, fcd, fco, fca, toolconf, backup, labels):
713 713 tool, toolpath, binary, symlink, scriptfn = toolconf
714 714 uipathfn = scmutil.getuipathfn(repo)
715 715 if fcd.isabsent() or fco.isabsent():
716 716 repo.ui.warn(
717 717 _(b'warning: %s cannot merge change/delete conflict for %s\n')
718 718 % (tool, uipathfn(fcd.path()))
719 719 )
720 720 return False, 1, None
721 721 localpath = _workingpath(repo, fcd)
722 722 args = _toolstr(repo.ui, tool, b"args")
723 723
724 724 with _maketempfiles(
725 725 repo, fco, fca, repo.wvfs.join(backup.path()), b"$output" in args
726 726 ) as temppaths:
727 727 basepath, otherpath, localoutputpath = temppaths
728 728 outpath = b""
729 729 mylabel, otherlabel = labels[:2]
730 730 baselabel = labels[2]
731 731 env = {
732 732 b'HG_FILE': fcd.path(),
733 733 b'HG_MY_NODE': short(mynode),
734 734 b'HG_OTHER_NODE': short(fco.changectx().node()),
735 735 b'HG_BASE_NODE': short(fca.changectx().node()),
736 736 b'HG_MY_ISLINK': b'l' in fcd.flags(),
737 737 b'HG_OTHER_ISLINK': b'l' in fco.flags(),
738 738 b'HG_BASE_ISLINK': b'l' in fca.flags(),
739 739 b'HG_MY_LABEL': mylabel,
740 740 b'HG_OTHER_LABEL': otherlabel,
741 741 b'HG_BASE_LABEL': baselabel,
742 742 }
743 743 ui = repo.ui
744 744
745 745 if b"$output" in args:
746 746 # read input from backup, write to original
747 747 outpath = localpath
748 748 localpath = localoutputpath
749 749 replace = {
750 750 b'local': localpath,
751 751 b'base': basepath,
752 752 b'other': otherpath,
753 753 b'output': outpath,
754 754 b'labellocal': mylabel,
755 755 b'labelother': otherlabel,
756 756 b'labelbase': baselabel,
757 757 }
758 758 args = util.interpolate(
759 759 br'\$',
760 760 replace,
761 761 args,
762 762 lambda s: procutil.shellquote(util.localpath(s)),
763 763 )
764 764 if _toolbool(ui, tool, b"gui"):
765 765 repo.ui.status(
766 766 _(b'running merge tool %s for file %s\n')
767 767 % (tool, uipathfn(fcd.path()))
768 768 )
769 769 if scriptfn is None:
770 770 cmd = toolpath + b' ' + args
771 771 repo.ui.debug(b'launching merge tool: %s\n' % cmd)
772 772 _describemerge(ui, repo, mynode, fcd, fca, fco, env, toolpath, args)
773 773 r = ui.system(
774 774 cmd, cwd=repo.root, environ=env, blockedtag=b'mergetool'
775 775 )
776 776 else:
777 777 repo.ui.debug(
778 778 b'launching python merge script: %s:%s\n' % (toolpath, scriptfn)
779 779 )
780 780 r = 0
781 781 try:
782 782 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
783 783 from . import extensions
784 784
785 785 mod = extensions.loadpath(toolpath, b'hgmerge.%s' % tool)
786 786 except Exception:
787 787 raise error.Abort(
788 788 _(b"loading python merge script failed: %s") % toolpath
789 789 )
790 790 mergefn = getattr(mod, scriptfn, None)
791 791 if mergefn is None:
792 792 raise error.Abort(
793 793 _(b"%s does not have function: %s") % (toolpath, scriptfn)
794 794 )
795 795 argslist = procutil.shellsplit(args)
796 796 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
797 797 from . import hook
798 798
799 799 ret, raised = hook.pythonhook(
800 800 ui, repo, b"merge", toolpath, mergefn, {b'args': argslist}, True
801 801 )
802 802 if raised:
803 803 r = 1
804 804 repo.ui.debug(b'merge tool returned: %d\n' % r)
805 805 return True, r, False
806 806
807 807
808 def _formatlabel(ctx, template, label, pad):
808 def _formatlabel(input, template, pad):
809 809 """Applies the given template to the ctx, prefixed by the label.
810 810
811 811 Pad is the minimum width of the label prefix, so that multiple markers
812 812 can have aligned templated parts.
813 813 """
814 ctx = input.fctx.changectx()
814 815 if ctx.node() is None:
815 816 ctx = ctx.p1()
816 817
817 818 props = {b'ctx': ctx}
818 819 templateresult = template.renderdefault(props)
819 820
820 label = (b'%s:' % label).ljust(pad + 1)
821 label = (b'%s:' % input.label).ljust(pad + 1)
821 822 mark = b'%s %s' % (label, templateresult)
822 823 mark = mark.splitlines()[0] # split for safety
823 824
824 825 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
825 return stringutil.ellipsis(mark, 80 - 8)
826 input.label = stringutil.ellipsis(mark, 80 - 8)
826 827
827 828
828 def _formatlabels(repo, fcd, fco, fca, labels, tool=None):
829 def _formatlabels(repo, inputs, tool=None):
829 830 """Formats the given labels using the conflict marker template.
830 831
831 832 Returns a list of formatted labels.
832 833 """
833 cd = fcd.changectx()
834 co = fco.changectx()
835 ca = fca.changectx()
836
837 834 ui = repo.ui
838 835 template = ui.config(b'command-templates', b'mergemarker')
839 836 if tool is not None:
840 837 template = _toolstr(ui, tool, b'mergemarkertemplate', template)
841 838 template = templater.unquotestring(template)
842 839 tres = formatter.templateresources(ui, repo)
843 840 tmpl = formatter.maketemplater(
844 841 ui, template, defaults=templatekw.keywords, resources=tres
845 842 )
846 843
847 pad = max(len(l) for l in labels)
844 pad = max(len(input.label) for input in inputs)
848 845
849 newlabels = [
850 _formatlabel(cd, tmpl, labels[0], pad),
851 _formatlabel(co, tmpl, labels[1], pad),
852 _formatlabel(ca, tmpl, labels[2], pad),
853 ]
854 return newlabels
846 return [_formatlabel(input, tmpl, pad) for input in inputs]
855 847
856 848
857 849 def partextras(labels):
858 850 """Return a dictionary of extra labels for use in prompts to the user
859 851
860 852 Intended use is in strings of the form "(l)ocal%(l)s".
861 853 """
862 854 if labels is None:
863 855 return {
864 856 b"l": b"",
865 857 b"o": b"",
866 858 }
867 859
868 860 return {
869 861 b"l": b" [%s]" % labels[0],
870 862 b"o": b" [%s]" % labels[1],
871 863 }
872 864
873 865
874 866 def _restorebackup(fcd, backup):
875 867 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
876 868 # util.copy here instead.
877 869 fcd.write(backup.data(), fcd.flags())
878 870
879 871
880 872 def _makebackup(repo, ui, wctx, fcd):
881 873 """Makes and returns a filectx-like object for ``fcd``'s backup file.
882 874
883 875 In addition to preserving the user's pre-existing modifications to `fcd`
884 876 (if any), the backup is used to undo certain premerges, confirm whether a
885 877 merge changed anything, and determine what line endings the new file should
886 878 have.
887 879
888 880 Backups only need to be written once since their content doesn't change
889 881 afterwards.
890 882 """
891 883 if fcd.isabsent():
892 884 return None
893 885 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
894 886 # merge -> filemerge). (I suspect the fileset import is the weakest link)
895 887 from . import context
896 888
897 889 backup = scmutil.backuppath(ui, repo, fcd.path())
898 890 inworkingdir = backup.startswith(repo.wvfs.base) and not backup.startswith(
899 891 repo.vfs.base
900 892 )
901 893 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
902 894 # If the backup file is to be in the working directory, and we're
903 895 # merging in-memory, we must redirect the backup to the memory context
904 896 # so we don't disturb the working directory.
905 897 relpath = backup[len(repo.wvfs.base) + 1 :]
906 898 wctx[relpath].write(fcd.data(), fcd.flags())
907 899 return wctx[relpath]
908 900 else:
909 901 # Otherwise, write to wherever path the user specified the backups
910 902 # should go. We still need to switch based on whether the source is
911 903 # in-memory so we can use the fast path of ``util.copy`` if both are
912 904 # on disk.
913 905 if isinstance(fcd, context.overlayworkingfilectx):
914 906 util.writefile(backup, fcd.data())
915 907 else:
916 908 a = _workingpath(repo, fcd)
917 909 util.copyfile(a, backup)
918 910 # A arbitraryfilectx is returned, so we can run the same functions on
919 911 # the backup context regardless of where it lives.
920 912 return context.arbitraryfilectx(backup, repo=repo)
921 913
922 914
923 915 @contextlib.contextmanager
924 916 def _maketempfiles(repo, fco, fca, localpath, uselocalpath):
925 917 """Writes out `fco` and `fca` as temporary files, and (if uselocalpath)
926 918 copies `localpath` to another temporary file, so an external merge tool may
927 919 use them.
928 920 """
929 921 tmproot = None
930 922 tmprootprefix = repo.ui.config(b'experimental', b'mergetempdirprefix')
931 923 if tmprootprefix:
932 924 tmproot = pycompat.mkdtemp(prefix=tmprootprefix)
933 925
934 926 def maketempfrompath(prefix, path):
935 927 fullbase, ext = os.path.splitext(path)
936 928 pre = b"%s~%s" % (os.path.basename(fullbase), prefix)
937 929 if tmproot:
938 930 name = os.path.join(tmproot, pre)
939 931 if ext:
940 932 name += ext
941 933 f = open(name, "wb")
942 934 else:
943 935 fd, name = pycompat.mkstemp(prefix=pre + b'.', suffix=ext)
944 936 f = os.fdopen(fd, "wb")
945 937 return f, name
946 938
947 939 def tempfromcontext(prefix, ctx):
948 940 f, name = maketempfrompath(prefix, ctx.path())
949 941 data = ctx.decodeddata()
950 942 f.write(data)
951 943 f.close()
952 944 return name
953 945
954 946 b = tempfromcontext(b"base", fca)
955 947 c = tempfromcontext(b"other", fco)
956 948 d = localpath
957 949 if uselocalpath:
958 950 # We start off with this being the backup filename, so remove the .orig
959 951 # to make syntax-highlighting more likely.
960 952 if d.endswith(b'.orig'):
961 953 d, _ = os.path.splitext(d)
962 954 f, d = maketempfrompath(b"local", d)
963 955 with open(localpath, b'rb') as src:
964 956 f.write(src.read())
965 957 f.close()
966 958
967 959 try:
968 960 yield b, c, d
969 961 finally:
970 962 if tmproot:
971 963 shutil.rmtree(tmproot)
972 964 else:
973 965 util.unlink(b)
974 966 util.unlink(c)
975 967 # if not uselocalpath, d is the 'orig'/backup file which we
976 968 # shouldn't delete.
977 969 if d and uselocalpath:
978 970 util.unlink(d)
979 971
980 972
981 973 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
982 974 """perform a 3-way merge in the working directory
983 975
984 976 mynode = parent node before merge
985 977 orig = original local filename before merge
986 978 fco = other file context
987 979 fca = ancestor file context
988 980 fcd = local file context for current/destination file
989 981
990 982 Returns whether the merge is complete, the return value of the merge, and
991 983 a boolean indicating whether the file was deleted from disk."""
992 984
993 985 if not fco.cmp(fcd): # files identical?
994 986 return None, False
995 987
996 988 ui = repo.ui
997 989 fd = fcd.path()
998 990 uipathfn = scmutil.getuipathfn(repo)
999 991 fduipath = uipathfn(fd)
1000 992 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
1001 993 symlink = b'l' in fcd.flags() + fco.flags()
1002 994 changedelete = fcd.isabsent() or fco.isabsent()
1003 995 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
1004 996 scriptfn = None
1005 997 if tool in internals and tool.startswith(b'internal:'):
1006 998 # normalize to new-style names (':merge' etc)
1007 999 tool = tool[len(b'internal') :]
1008 1000 if toolpath and toolpath.startswith(b'python:'):
1009 1001 invalidsyntax = False
1010 1002 if toolpath.count(b':') >= 2:
1011 1003 script, scriptfn = toolpath[7:].rsplit(b':', 1)
1012 1004 if not scriptfn:
1013 1005 invalidsyntax = True
1014 1006 # missing :callable can lead to spliting on windows drive letter
1015 1007 if b'\\' in scriptfn or b'/' in scriptfn:
1016 1008 invalidsyntax = True
1017 1009 else:
1018 1010 invalidsyntax = True
1019 1011 if invalidsyntax:
1020 1012 raise error.Abort(_(b"invalid 'python:' syntax: %s") % toolpath)
1021 1013 toolpath = script
1022 1014 ui.debug(
1023 1015 b"picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
1024 1016 % (
1025 1017 tool,
1026 1018 fduipath,
1027 1019 pycompat.bytestr(binary),
1028 1020 pycompat.bytestr(symlink),
1029 1021 pycompat.bytestr(changedelete),
1030 1022 )
1031 1023 )
1032 1024
1033 1025 if tool in internals:
1034 1026 func = internals[tool]
1035 1027 mergetype = func.mergetype
1036 1028 onfailure = func.onfailure
1037 1029 precheck = func.precheck
1038 1030 isexternal = False
1039 1031 else:
1040 1032 if wctx.isinmemory():
1041 1033 func = _xmergeimm
1042 1034 else:
1043 1035 func = _xmerge
1044 1036 mergetype = fullmerge
1045 1037 onfailure = _(b"merging %s failed!\n")
1046 1038 precheck = None
1047 1039 isexternal = True
1048 1040
1049 1041 toolconf = tool, toolpath, binary, symlink, scriptfn
1050 1042
1051 1043 if not labels:
1052 1044 labels = [b'local', b'other']
1053 1045 if len(labels) < 3:
1054 1046 labels.append(b'base')
1047 local = simplemerge.MergeInput(fcd, labels[0])
1048 other = simplemerge.MergeInput(fco, labels[1])
1049 base = simplemerge.MergeInput(fca, labels[2])
1055 1050 if mergetype == nomerge:
1056 return func(repo, mynode, fcd, fco, fca, toolconf, labels)
1051 return func(
1052 repo,
1053 mynode,
1054 fcd,
1055 fco,
1056 fca,
1057 toolconf,
1058 [local.label, other.label, base.label],
1059 )
1057 1060
1058 1061 if orig != fco.path():
1059 1062 ui.status(
1060 1063 _(b"merging %s and %s to %s\n")
1061 1064 % (uipathfn(orig), uipathfn(fco.path()), fduipath)
1062 1065 )
1063 1066 else:
1064 1067 ui.status(_(b"merging %s\n") % fduipath)
1065 1068
1066 1069 ui.debug(b"my %s other %s ancestor %s\n" % (fcd, fco, fca))
1067 1070
1068 1071 if precheck and not precheck(repo, mynode, fcd, fco, fca, toolconf):
1069 1072 if onfailure:
1070 1073 if wctx.isinmemory():
1071 1074 raise error.InMemoryMergeConflictsError(
1072 1075 b'in-memory merge does not support merge conflicts'
1073 1076 )
1074 1077 ui.warn(onfailure % fduipath)
1075 1078 return 1, False
1076 1079
1077 1080 backup = _makebackup(repo, ui, wctx, fcd)
1078 1081 r = 1
1079 1082 try:
1080 1083 internalmarkerstyle = ui.config(b'ui', b'mergemarkers')
1081 1084 if isexternal:
1082 1085 markerstyle = _toolstr(ui, tool, b'mergemarkers')
1083 1086 else:
1084 1087 markerstyle = internalmarkerstyle
1085 1088
1086 formattedlabels = labels
1087 if markerstyle != b'basic':
1088 formattedlabels = _formatlabels(
1089 repo, fcd, fco, fca, labels, tool=tool
1090 )
1091
1092 1089 if mergetype == fullmerge:
1093 1090 # conflict markers generated by premerge will use 'detailed'
1094 1091 # settings if either ui.mergemarkers or the tool's mergemarkers
1095 1092 # setting is 'detailed'. This way tools can have basic labels in
1096 1093 # space-constrained areas of the UI, but still get full information
1097 1094 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
1098 premergelabels = labels
1099 1095 labeltool = None
1100 1096 if markerstyle != b'basic':
1101 1097 # respect 'tool's mergemarkertemplate (which defaults to
1102 1098 # command-templates.mergemarker)
1103 1099 labeltool = tool
1104 1100 if internalmarkerstyle != b'basic' or markerstyle != b'basic':
1105 premergelabels = _formatlabels(
1106 repo, fcd, fco, fca, premergelabels, tool=labeltool
1107 )
1101 _formatlabels(repo, [local, other, base], tool=labeltool)
1108 1102
1109 1103 r = _premerge(
1110 repo, fcd, fco, fca, toolconf, backup, labels=premergelabels
1104 repo,
1105 fcd,
1106 fco,
1107 fca,
1108 toolconf,
1109 backup,
1110 labels=[local.label, other.label, base.label],
1111 1111 )
1112 1112 # we're done if premerge was successful (r is 0)
1113 1113 if not r:
1114 1114 return r, False
1115 1115
1116 # Reset to basic labels
1117 local.label = labels[0]
1118 other.label = labels[1]
1119 base.label = labels[2]
1120
1121 if markerstyle != b'basic':
1122 _formatlabels(repo, [local, other, base], tool=tool)
1123
1116 1124 needcheck, r, deleted = func(
1117 1125 repo,
1118 1126 mynode,
1119 1127 fcd,
1120 1128 fco,
1121 1129 fca,
1122 1130 toolconf,
1123 1131 backup,
1124 labels=formattedlabels,
1132 labels=[local.label, other.label, base.label],
1125 1133 )
1126 1134
1127 1135 if needcheck:
1128 1136 r = _check(repo, r, ui, tool, fcd, backup)
1129 1137
1130 1138 if r:
1131 1139 if onfailure:
1132 1140 if wctx.isinmemory():
1133 1141 raise error.InMemoryMergeConflictsError(
1134 1142 b'in-memory merge '
1135 1143 b'does not support '
1136 1144 b'merge conflicts'
1137 1145 )
1138 1146 ui.warn(onfailure % fduipath)
1139 1147 _onfilemergefailure(ui)
1140 1148
1141 1149 return r, deleted
1142 1150 finally:
1143 1151 if not r and backup is not None:
1144 1152 backup.remove()
1145 1153
1146 1154
1147 1155 def _haltmerge():
1148 1156 msg = _(b'merge halted after failed merge (see hg resolve)')
1149 1157 raise error.InterventionRequired(msg)
1150 1158
1151 1159
1152 1160 def _onfilemergefailure(ui):
1153 1161 action = ui.config(b'merge', b'on-failure')
1154 1162 if action == b'prompt':
1155 1163 msg = _(b'continue merge operation (yn)?$$ &Yes $$ &No')
1156 1164 if ui.promptchoice(msg, 0) == 1:
1157 1165 _haltmerge()
1158 1166 if action == b'halt':
1159 1167 _haltmerge()
1160 1168 # default action is 'continue', in which case we neither prompt nor halt
1161 1169
1162 1170
1163 1171 def hasconflictmarkers(data):
1164 1172 # Detect lines starting with a string of 7 identical characters from the
1165 1173 # subset Mercurial uses for conflict markers, followed by either the end of
1166 1174 # line or a space and some text. Note that using [<>=+|-]{7} would detect
1167 1175 # `<><><><><` as a conflict marker, which we don't want.
1168 1176 return bool(
1169 1177 re.search(
1170 1178 br"^([<>=+|-])\1{6}( .*)$",
1171 1179 data,
1172 1180 re.MULTILINE,
1173 1181 )
1174 1182 )
1175 1183
1176 1184
1177 1185 def _check(repo, r, ui, tool, fcd, backup):
1178 1186 fd = fcd.path()
1179 1187 uipathfn = scmutil.getuipathfn(repo)
1180 1188
1181 1189 if not r and (
1182 1190 _toolbool(ui, tool, b"checkconflicts")
1183 1191 or b'conflicts' in _toollist(ui, tool, b"check")
1184 1192 ):
1185 1193 if hasconflictmarkers(fcd.data()):
1186 1194 r = 1
1187 1195
1188 1196 checked = False
1189 1197 if b'prompt' in _toollist(ui, tool, b"check"):
1190 1198 checked = True
1191 1199 if ui.promptchoice(
1192 1200 _(b"was merge of '%s' successful (yn)?$$ &Yes $$ &No")
1193 1201 % uipathfn(fd),
1194 1202 1,
1195 1203 ):
1196 1204 r = 1
1197 1205
1198 1206 if (
1199 1207 not r
1200 1208 and not checked
1201 1209 and (
1202 1210 _toolbool(ui, tool, b"checkchanged")
1203 1211 or b'changed' in _toollist(ui, tool, b"check")
1204 1212 )
1205 1213 ):
1206 1214 if backup is not None and not fcd.cmp(backup):
1207 1215 if ui.promptchoice(
1208 1216 _(
1209 1217 b" output file %s appears unchanged\n"
1210 1218 b"was merge successful (yn)?"
1211 1219 b"$$ &Yes $$ &No"
1212 1220 )
1213 1221 % uipathfn(fd),
1214 1222 1,
1215 1223 ):
1216 1224 r = 1
1217 1225
1218 1226 if backup is not None and _toolbool(ui, tool, b"fixeol"):
1219 1227 _matcheol(_workingpath(repo, fcd), backup)
1220 1228
1221 1229 return r
1222 1230
1223 1231
1224 1232 def _workingpath(repo, ctx):
1225 1233 return repo.wjoin(ctx.path())
1226 1234
1227 1235
1228 1236 def loadinternalmerge(ui, extname, registrarobj):
1229 1237 """Load internal merge tool from specified registrarobj"""
1230 1238 for name, func in pycompat.iteritems(registrarobj._table):
1231 1239 fullname = b':' + name
1232 1240 internals[fullname] = func
1233 1241 internals[b'internal:' + name] = func
1234 1242 internalsdoc[fullname] = func
1235 1243
1236 1244 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1237 1245 if capabilities:
1238 1246 capdesc = b" (actual capabilities: %s)" % b', '.join(
1239 1247 capabilities
1240 1248 )
1241 1249 func.__doc__ = func.__doc__ + pycompat.sysstr(b"\n\n%s" % capdesc)
1242 1250
1243 1251 # to put i18n comments into hg.pot for automatically generated texts
1244 1252
1245 1253 # i18n: "binary" and "symlink" are keywords
1246 1254 # i18n: this text is added automatically
1247 1255 _(b" (actual capabilities: binary, symlink)")
1248 1256 # i18n: "binary" is keyword
1249 1257 # i18n: this text is added automatically
1250 1258 _(b" (actual capabilities: binary)")
1251 1259 # i18n: "symlink" is keyword
1252 1260 # i18n: this text is added automatically
1253 1261 _(b" (actual capabilities: symlink)")
1254 1262
1255 1263
1256 1264 # load built-in merge tools explicitly to setup internalsdoc
1257 1265 loadinternalmerge(None, None, internaltool)
1258 1266
1259 1267 # tell hggettext to extract docstrings from these functions:
1260 1268 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now