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