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