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