##// END OF EJS Templates
merge: allow user to halt merge on merge-tool failures...
Ryan McElroy -
r34798:284fa44f default
parent child Browse files
Show More
@@ -1,817 +1,818 b''
1 1 # filemerge.py - file-level merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import os
11 11 import re
12 12 import tempfile
13 13
14 14 from .i18n import _
15 15 from .node import nullid, short
16 16
17 17 from . import (
18 18 encoding,
19 19 error,
20 20 formatter,
21 21 match,
22 22 pycompat,
23 23 registrar,
24 24 scmutil,
25 25 simplemerge,
26 26 tagmerge,
27 27 templatekw,
28 28 templater,
29 29 util,
30 30 )
31 31
32 32 def _toolstr(ui, tool, part, default=""):
33 33 return ui.config("merge-tools", tool + "." + part, default)
34 34
35 35 def _toolbool(ui, tool, part, default=False):
36 36 return ui.configbool("merge-tools", tool + "." + part, default)
37 37
38 38 def _toollist(ui, tool, part, default=None):
39 39 if default is None:
40 40 default = []
41 41 return ui.configlist("merge-tools", tool + "." + part, default)
42 42
43 43 internals = {}
44 44 # Merge tools to document.
45 45 internalsdoc = {}
46 46
47 47 internaltool = registrar.internalmerge()
48 48
49 49 # internal tool merge types
50 50 nomerge = internaltool.nomerge
51 51 mergeonly = internaltool.mergeonly # just the full merge, no premerge
52 52 fullmerge = internaltool.fullmerge # both premerge and merge
53 53
54 54 _localchangedotherdeletedmsg = _(
55 55 "local%(l)s changed %(fd)s which other%(o)s deleted\n"
56 56 "use (c)hanged version, (d)elete, or leave (u)nresolved?"
57 57 "$$ &Changed $$ &Delete $$ &Unresolved")
58 58
59 59 _otherchangedlocaldeletedmsg = _(
60 60 "other%(o)s changed %(fd)s which local%(l)s deleted\n"
61 61 "use (c)hanged version, leave (d)eleted, or "
62 62 "leave (u)nresolved?"
63 63 "$$ &Changed $$ &Deleted $$ &Unresolved")
64 64
65 65 class absentfilectx(object):
66 66 """Represents a file that's ostensibly in a context but is actually not
67 67 present in it.
68 68
69 69 This is here because it's very specific to the filemerge code for now --
70 70 other code is likely going to break with the values this returns."""
71 71 def __init__(self, ctx, f):
72 72 self._ctx = ctx
73 73 self._f = f
74 74
75 75 def path(self):
76 76 return self._f
77 77
78 78 def size(self):
79 79 return None
80 80
81 81 def data(self):
82 82 return None
83 83
84 84 def filenode(self):
85 85 return nullid
86 86
87 87 _customcmp = True
88 88 def cmp(self, fctx):
89 89 """compare with other file context
90 90
91 91 returns True if different from fctx.
92 92 """
93 93 return not (fctx.isabsent() and
94 94 fctx.ctx() == self.ctx() and
95 95 fctx.path() == self.path())
96 96
97 97 def flags(self):
98 98 return ''
99 99
100 100 def changectx(self):
101 101 return self._ctx
102 102
103 103 def isbinary(self):
104 104 return False
105 105
106 106 def isabsent(self):
107 107 return True
108 108
109 109 def _findtool(ui, tool):
110 110 if tool in internals:
111 111 return tool
112 112 return findexternaltool(ui, tool)
113 113
114 114 def findexternaltool(ui, tool):
115 115 for kn in ("regkey", "regkeyalt"):
116 116 k = _toolstr(ui, tool, kn)
117 117 if not k:
118 118 continue
119 119 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
120 120 if p:
121 121 p = util.findexe(p + _toolstr(ui, tool, "regappend"))
122 122 if p:
123 123 return p
124 124 exe = _toolstr(ui, tool, "executable", tool)
125 125 return util.findexe(util.expandpath(exe))
126 126
127 127 def _picktool(repo, ui, path, binary, symlink, changedelete):
128 128 def supportscd(tool):
129 129 return tool in internals and internals[tool].mergetype == nomerge
130 130
131 131 def check(tool, pat, symlink, binary, changedelete):
132 132 tmsg = tool
133 133 if pat:
134 134 tmsg = _("%s (for pattern %s)") % (tool, pat)
135 135 if not _findtool(ui, tool):
136 136 if pat: # explicitly requested tool deserves a warning
137 137 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
138 138 else: # configured but non-existing tools are more silent
139 139 ui.note(_("couldn't find merge tool %s\n") % tmsg)
140 140 elif symlink and not _toolbool(ui, tool, "symlink"):
141 141 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
142 142 elif binary and not _toolbool(ui, tool, "binary"):
143 143 ui.warn(_("tool %s can't handle binary\n") % tmsg)
144 144 elif changedelete and not supportscd(tool):
145 145 # the nomerge tools are the only tools that support change/delete
146 146 # conflicts
147 147 pass
148 148 elif not util.gui() and _toolbool(ui, tool, "gui"):
149 149 ui.warn(_("tool %s requires a GUI\n") % tmsg)
150 150 else:
151 151 return True
152 152 return False
153 153
154 154 # internal config: ui.forcemerge
155 155 # forcemerge comes from command line arguments, highest priority
156 156 force = ui.config('ui', 'forcemerge')
157 157 if force:
158 158 toolpath = _findtool(ui, force)
159 159 if changedelete and not supportscd(toolpath):
160 160 return ":prompt", None
161 161 else:
162 162 if toolpath:
163 163 return (force, util.shellquote(toolpath))
164 164 else:
165 165 # mimic HGMERGE if given tool not found
166 166 return (force, force)
167 167
168 168 # HGMERGE takes next precedence
169 169 hgmerge = encoding.environ.get("HGMERGE")
170 170 if hgmerge:
171 171 if changedelete and not supportscd(hgmerge):
172 172 return ":prompt", None
173 173 else:
174 174 return (hgmerge, hgmerge)
175 175
176 176 # then patterns
177 177 for pat, tool in ui.configitems("merge-patterns"):
178 178 mf = match.match(repo.root, '', [pat])
179 179 if mf(path) and check(tool, pat, symlink, False, changedelete):
180 180 toolpath = _findtool(ui, tool)
181 181 return (tool, util.shellquote(toolpath))
182 182
183 183 # then merge tools
184 184 tools = {}
185 185 disabled = set()
186 186 for k, v in ui.configitems("merge-tools"):
187 187 t = k.split('.')[0]
188 188 if t not in tools:
189 189 tools[t] = int(_toolstr(ui, t, "priority", "0"))
190 190 if _toolbool(ui, t, "disabled", False):
191 191 disabled.add(t)
192 192 names = tools.keys()
193 193 tools = sorted([(-p, tool) for tool, p in tools.items()
194 194 if tool not in disabled])
195 195 uimerge = ui.config("ui", "merge")
196 196 if uimerge:
197 197 # external tools defined in uimerge won't be able to handle
198 198 # change/delete conflicts
199 199 if uimerge not in names and not changedelete:
200 200 return (uimerge, uimerge)
201 201 tools.insert(0, (None, uimerge)) # highest priority
202 202 tools.append((None, "hgmerge")) # the old default, if found
203 203 for p, t in tools:
204 204 if check(t, None, symlink, binary, changedelete):
205 205 toolpath = _findtool(ui, t)
206 206 return (t, util.shellquote(toolpath))
207 207
208 208 # internal merge or prompt as last resort
209 209 if symlink or binary or changedelete:
210 210 if not changedelete and len(tools):
211 211 # any tool is rejected by capability for symlink or binary
212 212 ui.warn(_("no tool found to merge %s\n") % path)
213 213 return ":prompt", None
214 214 return ":merge", None
215 215
216 216 def _eoltype(data):
217 217 "Guess the EOL type of a file"
218 218 if '\0' in data: # binary
219 219 return None
220 220 if '\r\n' in data: # Windows
221 221 return '\r\n'
222 222 if '\r' in data: # Old Mac
223 223 return '\r'
224 224 if '\n' in data: # UNIX
225 225 return '\n'
226 226 return None # unknown
227 227
228 228 def _matcheol(file, back):
229 229 "Convert EOL markers in a file to match origfile"
230 230 tostyle = _eoltype(back.data()) # No repo.wread filters?
231 231 if tostyle:
232 232 data = util.readfile(file)
233 233 style = _eoltype(data)
234 234 if style:
235 235 newdata = data.replace(style, tostyle)
236 236 if newdata != data:
237 237 util.writefile(file, newdata)
238 238
239 239 @internaltool('prompt', nomerge)
240 240 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
241 241 """Asks the user which of the local `p1()` or the other `p2()` version to
242 242 keep as the merged version."""
243 243 ui = repo.ui
244 244 fd = fcd.path()
245 245
246 246 prompts = partextras(labels)
247 247 prompts['fd'] = fd
248 248 try:
249 249 if fco.isabsent():
250 250 index = ui.promptchoice(
251 251 _localchangedotherdeletedmsg % prompts, 2)
252 252 choice = ['local', 'other', 'unresolved'][index]
253 253 elif fcd.isabsent():
254 254 index = ui.promptchoice(
255 255 _otherchangedlocaldeletedmsg % prompts, 2)
256 256 choice = ['other', 'local', 'unresolved'][index]
257 257 else:
258 258 index = ui.promptchoice(
259 259 _("keep (l)ocal%(l)s, take (o)ther%(o)s, or leave (u)nresolved"
260 260 " for %(fd)s?"
261 261 "$$ &Local $$ &Other $$ &Unresolved") % prompts, 2)
262 262 choice = ['local', 'other', 'unresolved'][index]
263 263
264 264 if choice == 'other':
265 265 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf,
266 266 labels)
267 267 elif choice == 'local':
268 268 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf,
269 269 labels)
270 270 elif choice == 'unresolved':
271 271 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
272 272 labels)
273 273 except error.ResponseExpected:
274 274 ui.write("\n")
275 275 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
276 276 labels)
277 277
278 278 @internaltool('local', nomerge)
279 279 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
280 280 """Uses the local `p1()` version of files as the merged version."""
281 281 return 0, fcd.isabsent()
282 282
283 283 @internaltool('other', nomerge)
284 284 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
285 285 """Uses the other `p2()` version of files as the merged version."""
286 286 if fco.isabsent():
287 287 # local changed, remote deleted -- 'deleted' picked
288 288 _underlyingfctxifabsent(fcd).remove()
289 289 deleted = True
290 290 else:
291 291 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
292 292 deleted = False
293 293 return 0, deleted
294 294
295 295 @internaltool('fail', nomerge)
296 296 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
297 297 """
298 298 Rather than attempting to merge files that were modified on both
299 299 branches, it marks them as unresolved. The resolve command must be
300 300 used to resolve these conflicts."""
301 301 # for change/delete conflicts write out the changed version, then fail
302 302 if fcd.isabsent():
303 303 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
304 304 return 1, False
305 305
306 306 def _underlyingfctxifabsent(filectx):
307 307 """Sometimes when resolving, our fcd is actually an absentfilectx, but
308 308 we want to write to it (to do the resolve). This helper returns the
309 309 underyling workingfilectx in that case.
310 310 """
311 311 if filectx.isabsent():
312 312 return filectx.changectx()[filectx.path()]
313 313 else:
314 314 return filectx
315 315
316 316 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
317 317 tool, toolpath, binary, symlink = toolconf
318 318 if symlink or fcd.isabsent() or fco.isabsent():
319 319 return 1
320 320 unused, unused, unused, back = files
321 321
322 322 ui = repo.ui
323 323
324 324 validkeep = ['keep', 'keep-merge3']
325 325
326 326 # do we attempt to simplemerge first?
327 327 try:
328 328 premerge = _toolbool(ui, tool, "premerge", not binary)
329 329 except error.ConfigError:
330 330 premerge = _toolstr(ui, tool, "premerge").lower()
331 331 if premerge not in validkeep:
332 332 _valid = ', '.join(["'" + v + "'" for v in validkeep])
333 333 raise error.ConfigError(_("%s.premerge not valid "
334 334 "('%s' is neither boolean nor %s)") %
335 335 (tool, premerge, _valid))
336 336
337 337 if premerge:
338 338 if premerge == 'keep-merge3':
339 339 if not labels:
340 340 labels = _defaultconflictlabels
341 341 if len(labels) < 3:
342 342 labels.append('base')
343 343 r = simplemerge.simplemerge(ui, fcd, fca, fco, quiet=True, label=labels)
344 344 if not r:
345 345 ui.debug(" premerge successful\n")
346 346 return 0
347 347 if premerge not in validkeep:
348 348 # restore from backup and try again
349 349 _restorebackup(fcd, back)
350 350 return 1 # continue merging
351 351
352 352 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
353 353 tool, toolpath, binary, symlink = toolconf
354 354 if symlink:
355 355 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
356 356 'for %s\n') % (tool, fcd.path()))
357 357 return False
358 358 if fcd.isabsent() or fco.isabsent():
359 359 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
360 360 'conflict for %s\n') % (tool, fcd.path()))
361 361 return False
362 362 return True
363 363
364 364 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
365 365 """
366 366 Uses the internal non-interactive simple merge algorithm for merging
367 367 files. It will fail if there are any conflicts and leave markers in
368 368 the partially merged file. Markers will have two sections, one for each side
369 369 of merge, unless mode equals 'union' which suppresses the markers."""
370 370 ui = repo.ui
371 371
372 372 r = simplemerge.simplemerge(ui, fcd, fca, fco, label=labels, mode=mode)
373 373 return True, r, False
374 374
375 375 @internaltool('union', fullmerge,
376 376 _("warning: conflicts while merging %s! "
377 377 "(edit, then use 'hg resolve --mark')\n"),
378 378 precheck=_mergecheck)
379 379 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
380 380 """
381 381 Uses the internal non-interactive simple merge algorithm for merging
382 382 files. It will use both left and right sides for conflict regions.
383 383 No markers are inserted."""
384 384 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
385 385 files, labels, 'union')
386 386
387 387 @internaltool('merge', fullmerge,
388 388 _("warning: conflicts while merging %s! "
389 389 "(edit, then use 'hg resolve --mark')\n"),
390 390 precheck=_mergecheck)
391 391 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
392 392 """
393 393 Uses the internal non-interactive simple merge algorithm for merging
394 394 files. It will fail if there are any conflicts and leave markers in
395 395 the partially merged file. Markers will have two sections, one for each side
396 396 of merge."""
397 397 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
398 398 files, labels, 'merge')
399 399
400 400 @internaltool('merge3', fullmerge,
401 401 _("warning: conflicts while merging %s! "
402 402 "(edit, then use 'hg resolve --mark')\n"),
403 403 precheck=_mergecheck)
404 404 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
405 405 """
406 406 Uses the internal non-interactive simple merge algorithm for merging
407 407 files. It will fail if there are any conflicts and leave markers in
408 408 the partially merged file. Marker will have three sections, one from each
409 409 side of the merge and one for the base content."""
410 410 if not labels:
411 411 labels = _defaultconflictlabels
412 412 if len(labels) < 3:
413 413 labels.append('base')
414 414 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
415 415
416 416 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
417 417 labels=None, localorother=None):
418 418 """
419 419 Generic driver for _imergelocal and _imergeother
420 420 """
421 421 assert localorother is not None
422 422 tool, toolpath, binary, symlink = toolconf
423 423 r = simplemerge.simplemerge(repo.ui, fcd, fca, fco, label=labels,
424 424 localorother=localorother)
425 425 return True, r
426 426
427 427 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
428 428 def _imergelocal(*args, **kwargs):
429 429 """
430 430 Like :merge, but resolve all conflicts non-interactively in favor
431 431 of the local `p1()` changes."""
432 432 success, status = _imergeauto(localorother='local', *args, **kwargs)
433 433 return success, status, False
434 434
435 435 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
436 436 def _imergeother(*args, **kwargs):
437 437 """
438 438 Like :merge, but resolve all conflicts non-interactively in favor
439 439 of the other `p2()` changes."""
440 440 success, status = _imergeauto(localorother='other', *args, **kwargs)
441 441 return success, status, False
442 442
443 443 @internaltool('tagmerge', mergeonly,
444 444 _("automatic tag merging of %s failed! "
445 445 "(use 'hg resolve --tool :merge' or another merge "
446 446 "tool of your choice)\n"))
447 447 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
448 448 """
449 449 Uses the internal tag merge algorithm (experimental).
450 450 """
451 451 success, status = tagmerge.merge(repo, fcd, fco, fca)
452 452 return success, status, False
453 453
454 454 @internaltool('dump', fullmerge)
455 455 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
456 456 """
457 457 Creates three versions of the files to merge, containing the
458 458 contents of local, other and base. These files can then be used to
459 459 perform a merge manually. If the file to be merged is named
460 460 ``a.txt``, these files will accordingly be named ``a.txt.local``,
461 461 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
462 462 same directory as ``a.txt``.
463 463
464 464 This implies permerge. Therefore, files aren't dumped, if premerge
465 465 runs successfully. Use :forcedump to forcibly write files out.
466 466 """
467 467 a = _workingpath(repo, fcd)
468 468 fd = fcd.path()
469 469
470 470 # Run ``flushall()`` to make any missing folders the following wwrite
471 471 # calls might be depending on.
472 472 from . import context
473 473 if isinstance(fcd, context.overlayworkingfilectx):
474 474 fcd.ctx().flushall()
475 475
476 476 util.writefile(a + ".local", fcd.decodeddata())
477 477 repo.wwrite(fd + ".other", fco.data(), fco.flags())
478 478 repo.wwrite(fd + ".base", fca.data(), fca.flags())
479 479 return False, 1, False
480 480
481 481 @internaltool('forcedump', mergeonly)
482 482 def _forcedump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
483 483 labels=None):
484 484 """
485 485 Creates three versions of the files as same as :dump, but omits premerge.
486 486 """
487 487 return _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
488 488 labels=labels)
489 489
490 490 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
491 491 tool, toolpath, binary, symlink = toolconf
492 492 if fcd.isabsent() or fco.isabsent():
493 493 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
494 494 'for %s\n') % (tool, fcd.path()))
495 495 return False, 1, None
496 496 unused, unused, unused, back = files
497 497 a = _workingpath(repo, fcd)
498 498 b, c = _maketempfiles(repo, fco, fca)
499 499 try:
500 500 out = ""
501 501 env = {'HG_FILE': fcd.path(),
502 502 'HG_MY_NODE': short(mynode),
503 503 'HG_OTHER_NODE': str(fco.changectx()),
504 504 'HG_BASE_NODE': str(fca.changectx()),
505 505 'HG_MY_ISLINK': 'l' in fcd.flags(),
506 506 'HG_OTHER_ISLINK': 'l' in fco.flags(),
507 507 'HG_BASE_ISLINK': 'l' in fca.flags(),
508 508 }
509 509 ui = repo.ui
510 510
511 511 args = _toolstr(ui, tool, "args", '$local $base $other')
512 512 if "$output" in args:
513 513 # read input from backup, write to original
514 514 out = a
515 515 a = repo.wvfs.join(back.path())
516 516 replace = {'local': a, 'base': b, 'other': c, 'output': out}
517 517 args = util.interpolate(r'\$', replace, args,
518 518 lambda s: util.shellquote(util.localpath(s)))
519 519 cmd = toolpath + ' ' + args
520 520 if _toolbool(ui, tool, "gui"):
521 521 repo.ui.status(_('running merge tool %s for file %s\n') %
522 522 (tool, fcd.path()))
523 523 repo.ui.debug('launching merge tool: %s\n' % cmd)
524 524 r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool')
525 525 repo.ui.debug('merge tool returned: %d\n' % r)
526 526 return True, r, False
527 527 finally:
528 528 util.unlink(b)
529 529 util.unlink(c)
530 530
531 531 def _formatconflictmarker(repo, ctx, template, label, pad):
532 532 """Applies the given template to the ctx, prefixed by the label.
533 533
534 534 Pad is the minimum width of the label prefix, so that multiple markers
535 535 can have aligned templated parts.
536 536 """
537 537 if ctx.node() is None:
538 538 ctx = ctx.p1()
539 539
540 540 props = templatekw.keywords.copy()
541 541 props['templ'] = template
542 542 props['ctx'] = ctx
543 543 props['repo'] = repo
544 544 templateresult = template.render(props)
545 545
546 546 label = ('%s:' % label).ljust(pad + 1)
547 547 mark = '%s %s' % (label, templateresult)
548 548
549 549 if mark:
550 550 mark = mark.splitlines()[0] # split for safety
551 551
552 552 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
553 553 return util.ellipsis(mark, 80 - 8)
554 554
555 555 _defaultconflictlabels = ['local', 'other']
556 556
557 557 def _formatlabels(repo, fcd, fco, fca, labels):
558 558 """Formats the given labels using the conflict marker template.
559 559
560 560 Returns a list of formatted labels.
561 561 """
562 562 cd = fcd.changectx()
563 563 co = fco.changectx()
564 564 ca = fca.changectx()
565 565
566 566 ui = repo.ui
567 567 template = ui.config('ui', 'mergemarkertemplate')
568 568 template = templater.unquotestring(template)
569 569 tmpl = formatter.maketemplater(ui, template)
570 570
571 571 pad = max(len(l) for l in labels)
572 572
573 573 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
574 574 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
575 575 if len(labels) > 2:
576 576 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
577 577 return newlabels
578 578
579 579 def partextras(labels):
580 580 """Return a dictionary of extra labels for use in prompts to the user
581 581
582 582 Intended use is in strings of the form "(l)ocal%(l)s".
583 583 """
584 584 if labels is None:
585 585 return {
586 586 "l": "",
587 587 "o": "",
588 588 }
589 589
590 590 return {
591 591 "l": " [%s]" % labels[0],
592 592 "o": " [%s]" % labels[1],
593 593 }
594 594
595 595 def _restorebackup(fcd, back):
596 596 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
597 597 # util.copy here instead.
598 598 fcd.write(back.data(), fcd.flags())
599 599
600 600 def _makebackup(repo, ui, wctx, fcd, premerge):
601 601 """Makes and returns a filectx-like object for ``fcd``'s backup file.
602 602
603 603 In addition to preserving the user's pre-existing modifications to `fcd`
604 604 (if any), the backup is used to undo certain premerges, confirm whether a
605 605 merge changed anything, and determine what line endings the new file should
606 606 have.
607 607 """
608 608 if fcd.isabsent():
609 609 return None
610 610 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
611 611 # merge -> filemerge). (I suspect the fileset import is the weakest link)
612 612 from . import context
613 613 a = _workingpath(repo, fcd)
614 614 back = scmutil.origpath(ui, repo, a)
615 615 inworkingdir = (back.startswith(repo.wvfs.base) and not
616 616 back.startswith(repo.vfs.base))
617 617
618 618 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
619 619 # If the backup file is to be in the working directory, and we're
620 620 # merging in-memory, we must redirect the backup to the memory context
621 621 # so we don't disturb the working directory.
622 622 relpath = back[len(repo.wvfs.base) + 1:]
623 623 wctx[relpath].write(fcd.data(), fcd.flags())
624 624 return wctx[relpath]
625 625 else:
626 626 # Otherwise, write to wherever the user specified the backups should go.
627 627 #
628 628 # A arbitraryfilectx is returned, so we can run the same functions on
629 629 # the backup context regardless of where it lives.
630 630 if premerge:
631 631 util.copyfile(a, back)
632 632 return context.arbitraryfilectx(back, repo=repo)
633 633
634 634 def _maketempfiles(repo, fco, fca):
635 635 """Writes out `fco` and `fca` as temporary files, so an external merge
636 636 tool may use them.
637 637 """
638 638 def temp(prefix, ctx):
639 639 fullbase, ext = os.path.splitext(ctx.path())
640 640 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
641 641 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
642 642 data = repo.wwritedata(ctx.path(), ctx.data())
643 643 f = os.fdopen(fd, pycompat.sysstr("wb"))
644 644 f.write(data)
645 645 f.close()
646 646 return name
647 647
648 648 b = temp("base", fca)
649 649 c = temp("other", fco)
650 650
651 651 return b, c
652 652
653 653 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
654 654 """perform a 3-way merge in the working directory
655 655
656 656 premerge = whether this is a premerge
657 657 mynode = parent node before merge
658 658 orig = original local filename before merge
659 659 fco = other file context
660 660 fca = ancestor file context
661 661 fcd = local file context for current/destination file
662 662
663 663 Returns whether the merge is complete, the return value of the merge, and
664 664 a boolean indicating whether the file was deleted from disk."""
665 665
666 666 if not fco.cmp(fcd): # files identical?
667 667 return True, None, False
668 668
669 669 ui = repo.ui
670 670 fd = fcd.path()
671 671 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
672 672 symlink = 'l' in fcd.flags() + fco.flags()
673 673 changedelete = fcd.isabsent() or fco.isabsent()
674 674 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
675 675 if tool in internals and tool.startswith('internal:'):
676 676 # normalize to new-style names (':merge' etc)
677 677 tool = tool[len('internal'):]
678 678 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
679 679 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
680 680 pycompat.bytestr(changedelete)))
681 681
682 682 if tool in internals:
683 683 func = internals[tool]
684 684 mergetype = func.mergetype
685 685 onfailure = func.onfailure
686 686 precheck = func.precheck
687 687 else:
688 688 func = _xmerge
689 689 mergetype = fullmerge
690 690 onfailure = _("merging %s failed!\n")
691 691 precheck = None
692 692
693 693 # If using deferred writes, must flush any deferred contents if running
694 694 # an external merge tool since it has arbitrary access to the working
695 695 # copy.
696 696 wctx.flushall()
697 697
698 698 toolconf = tool, toolpath, binary, symlink
699 699
700 700 if mergetype == nomerge:
701 701 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
702 702 return True, r, deleted
703 703
704 704 if premerge:
705 705 if orig != fco.path():
706 706 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
707 707 else:
708 708 ui.status(_("merging %s\n") % fd)
709 709
710 710 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
711 711
712 712 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
713 713 toolconf):
714 714 if onfailure:
715 715 ui.warn(onfailure % fd)
716 716 return True, 1, False
717 717
718 718 back = _makebackup(repo, ui, wctx, fcd, premerge)
719 719 files = (None, None, None, back)
720 720 r = 1
721 721 try:
722 722 markerstyle = ui.config('ui', 'mergemarkers')
723 723 if not labels:
724 724 labels = _defaultconflictlabels
725 725 if markerstyle != 'basic':
726 726 labels = _formatlabels(repo, fcd, fco, fca, labels)
727 727
728 728 if premerge and mergetype == fullmerge:
729 729 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
730 730 # complete if premerge successful (r is 0)
731 731 return not r, r, False
732 732
733 733 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
734 734 toolconf, files, labels=labels)
735 735
736 736 if needcheck:
737 737 r = _check(repo, r, ui, tool, fcd, files)
738 738
739 739 if r:
740 740 if onfailure:
741 741 ui.warn(onfailure % fd)
742 _onfilemergefailure(ui)
742 743
743 744 return True, r, deleted
744 745 finally:
745 746 if not r and back is not None:
746 747 back.remove()
747 748
748 749 def _haltmerge():
749 750 msg = _('merge halted after failed merge (see hg resolve)')
750 751 raise error.InterventionRequired(msg)
751 752
752 753 def _onfilemergefailure(ui):
753 754 action = ui.config('merge', 'on-failure')
754 755 if action == 'prompt':
755 756 msg = _('continue merge operation (yn)?' '$$ &Yes $$ &No')
756 757 if ui.promptchoice(msg, 0) == 1:
757 758 _haltmerge()
758 759 if action == 'halt':
759 760 _haltmerge()
760 761 # default action is 'continue', in which case we neither prompt nor halt
761 762
762 763 def _check(repo, r, ui, tool, fcd, files):
763 764 fd = fcd.path()
764 765 unused, unused, unused, back = files
765 766
766 767 if not r and (_toolbool(ui, tool, "checkconflicts") or
767 768 'conflicts' in _toollist(ui, tool, "check")):
768 769 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
769 770 re.MULTILINE):
770 771 r = 1
771 772
772 773 checked = False
773 774 if 'prompt' in _toollist(ui, tool, "check"):
774 775 checked = True
775 776 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
776 777 "$$ &Yes $$ &No") % fd, 1):
777 778 r = 1
778 779
779 780 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
780 781 'changed' in
781 782 _toollist(ui, tool, "check")):
782 783 if back is not None and not fcd.cmp(back):
783 784 if ui.promptchoice(_(" output file %s appears unchanged\n"
784 785 "was merge successful (yn)?"
785 786 "$$ &Yes $$ &No") % fd, 1):
786 787 r = 1
787 788
788 789 if back is not None and _toolbool(ui, tool, "fixeol"):
789 790 _matcheol(_workingpath(repo, fcd), back)
790 791
791 792 return r
792 793
793 794 def _workingpath(repo, ctx):
794 795 return repo.wjoin(ctx.path())
795 796
796 797 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
797 798 return _filemerge(True, repo, wctx, mynode, orig, fcd, fco, fca,
798 799 labels=labels)
799 800
800 801 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
801 802 return _filemerge(False, repo, wctx, mynode, orig, fcd, fco, fca,
802 803 labels=labels)
803 804
804 805 def loadinternalmerge(ui, extname, registrarobj):
805 806 """Load internal merge tool from specified registrarobj
806 807 """
807 808 for name, func in registrarobj._table.iteritems():
808 809 fullname = ':' + name
809 810 internals[fullname] = func
810 811 internals['internal:' + name] = func
811 812 internalsdoc[fullname] = func
812 813
813 814 # load built-in merge tools explicitly to setup internalsdoc
814 815 loadinternalmerge(None, None, internaltool)
815 816
816 817 # tell hggettext to extract docstrings from these functions:
817 818 i18nfunctions = internals.values()
@@ -1,78 +1,79 b''
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extensions]
3 3 > rebase=
4 4 > [phases]
5 5 > publish=False
6 6 > [merge]
7 7 > EOF
8 8
9 9 $ hg init repo
10 10 $ cd repo
11 11 $ echo a > a
12 12 $ echo b > b
13 13 $ hg commit -qAm ab
14 14 $ echo c >> a
15 15 $ echo c >> b
16 16 $ hg commit -qAm c
17 17 $ hg up -q ".^"
18 18 $ echo d >> a
19 19 $ echo d >> b
20 20 $ hg commit -qAm d
21 21
22 22 Testing on-failure=continue
23 23 $ echo on-failure=continue >> $HGRCPATH
24 24 $ hg rebase -s 1 -d 2 --tool false
25 25 rebasing 1:1f28a51c3c9b "c"
26 26 merging a
27 27 merging b
28 28 merging a failed!
29 29 merging b failed!
30 30 unresolved conflicts (see hg resolve, then hg rebase --continue)
31 31 [1]
32 32
33 33 $ hg resolve --list
34 34 U a
35 35 U b
36 36
37 37 $ hg rebase --abort
38 38 rebase aborted
39 39
40 40 Testing on-failure=halt
41 41 $ echo on-failure=halt >> $HGRCPATH
42 42 $ hg rebase -s 1 -d 2 --tool false
43 43 rebasing 1:1f28a51c3c9b "c"
44 44 merging a
45 45 merging b
46 46 merging a failed!
47 merging b failed!
48 unresolved conflicts (see hg resolve, then hg rebase --continue)
47 merge halted after failed merge (see hg resolve)
49 48 [1]
50 49
51 50 $ hg resolve --list
52 51 U a
53 52 U b
54 53
55 54 $ hg rebase --abort
56 55 rebase aborted
57 56
58 57 Testing on-failure=prompt
59 58 $ echo on-failure=prompt >> $HGRCPATH
60 59 $ cat <<EOS | hg rebase -s 1 -d 2 --tool false --config ui.interactive=1
61 60 > y
62 61 > n
63 62 > EOS
64 63 rebasing 1:1f28a51c3c9b "c"
65 64 merging a
66 65 merging b
67 66 merging a failed!
67 continue merge operation (yn)? y
68 68 merging b failed!
69 unresolved conflicts (see hg resolve, then hg rebase --continue)
69 continue merge operation (yn)? n
70 merge halted after failed merge (see hg resolve)
70 71 [1]
71 72
72 73 $ hg resolve --list
73 74 U a
74 75 U b
75 76
76 77 $ hg rebase --abort
77 78 rebase aborted
78 79
General Comments 0
You need to be logged in to leave comments. Login now