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