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