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