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