##// END OF EJS Templates
filemerge: pass contexts to simplemerge...
Phil Cohen -
r33826:db3e9f7c default
parent child Browse files
Show More
@@ -1,747 +1,750 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 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 a, b, c, 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 r = simplemerge.simplemerge(ui, a, b, c, quiet=True, label=labels)
344 r = simplemerge.simplemerge(ui, a, b, c, fcd, fca, fco,
345 quiet=True, label=labels, repo=repo)
345 346 if not r:
346 347 ui.debug(" premerge successful\n")
347 348 return 0
348 349 if premerge not in validkeep:
349 350 util.copyfile(back, a) # restore from backup and try again
350 351 return 1 # continue merging
351 352
352 353 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
353 354 tool, toolpath, binary, symlink = toolconf
354 355 if symlink:
355 356 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
356 357 'for %s\n') % (tool, fcd.path()))
357 358 return False
358 359 if fcd.isabsent() or fco.isabsent():
359 360 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
360 361 'conflict for %s\n') % (tool, fcd.path()))
361 362 return False
362 363 return True
363 364
364 365 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
365 366 """
366 367 Uses the internal non-interactive simple merge algorithm for merging
367 368 files. It will fail if there are any conflicts and leave markers in
368 369 the partially merged file. Markers will have two sections, one for each side
369 370 of merge, unless mode equals 'union' which suppresses the markers."""
370 371 a, b, c, back = files
371 372
372 373 ui = repo.ui
373 374
374 r = simplemerge.simplemerge(ui, a, b, c, label=labels, mode=mode)
375 r = simplemerge.simplemerge(ui, a, b, c, fcd, fca, fco,
376 label=labels, mode=mode, repo=repo)
375 377 return True, r, False
376 378
377 379 @internaltool('union', fullmerge,
378 380 _("warning: conflicts while merging %s! "
379 381 "(edit, then use 'hg resolve --mark')\n"),
380 382 precheck=_mergecheck)
381 383 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
382 384 """
383 385 Uses the internal non-interactive simple merge algorithm for merging
384 386 files. It will use both left and right sides for conflict regions.
385 387 No markers are inserted."""
386 388 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
387 389 files, labels, 'union')
388 390
389 391 @internaltool('merge', fullmerge,
390 392 _("warning: conflicts while merging %s! "
391 393 "(edit, then use 'hg resolve --mark')\n"),
392 394 precheck=_mergecheck)
393 395 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
394 396 """
395 397 Uses the internal non-interactive simple merge algorithm for merging
396 398 files. It will fail if there are any conflicts and leave markers in
397 399 the partially merged file. Markers will have two sections, one for each side
398 400 of merge."""
399 401 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
400 402 files, labels, 'merge')
401 403
402 404 @internaltool('merge3', fullmerge,
403 405 _("warning: conflicts while merging %s! "
404 406 "(edit, then use 'hg resolve --mark')\n"),
405 407 precheck=_mergecheck)
406 408 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
407 409 """
408 410 Uses the internal non-interactive simple merge algorithm for merging
409 411 files. It will fail if there are any conflicts and leave markers in
410 412 the partially merged file. Marker will have three sections, one from each
411 413 side of the merge and one for the base content."""
412 414 if not labels:
413 415 labels = _defaultconflictlabels
414 416 if len(labels) < 3:
415 417 labels.append('base')
416 418 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
417 419
418 420 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
419 421 labels=None, localorother=None):
420 422 """
421 423 Generic driver for _imergelocal and _imergeother
422 424 """
423 425 assert localorother is not None
424 426 tool, toolpath, binary, symlink = toolconf
425 427 a, b, c, back = files
426 r = simplemerge.simplemerge(repo.ui, a, b, c, label=labels,
427 localorother=localorother)
428 r = simplemerge.simplemerge(repo.ui, a, b, c, fcd, fca, fco,
429 label=labels, localorother=localorother,
430 repo=repo)
428 431 return True, r
429 432
430 433 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
431 434 def _imergelocal(*args, **kwargs):
432 435 """
433 436 Like :merge, but resolve all conflicts non-interactively in favor
434 437 of the local `p1()` changes."""
435 438 success, status = _imergeauto(localorother='local', *args, **kwargs)
436 439 return success, status, False
437 440
438 441 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
439 442 def _imergeother(*args, **kwargs):
440 443 """
441 444 Like :merge, but resolve all conflicts non-interactively in favor
442 445 of the other `p2()` changes."""
443 446 success, status = _imergeauto(localorother='other', *args, **kwargs)
444 447 return success, status, False
445 448
446 449 @internaltool('tagmerge', mergeonly,
447 450 _("automatic tag merging of %s failed! "
448 451 "(use 'hg resolve --tool :merge' or another merge "
449 452 "tool of your choice)\n"))
450 453 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
451 454 """
452 455 Uses the internal tag merge algorithm (experimental).
453 456 """
454 457 success, status = tagmerge.merge(repo, fcd, fco, fca)
455 458 return success, status, False
456 459
457 460 @internaltool('dump', fullmerge)
458 461 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
459 462 """
460 463 Creates three versions of the files to merge, containing the
461 464 contents of local, other and base. These files can then be used to
462 465 perform a merge manually. If the file to be merged is named
463 466 ``a.txt``, these files will accordingly be named ``a.txt.local``,
464 467 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
465 468 same directory as ``a.txt``.
466 469
467 470 This implies permerge. Therefore, files aren't dumped, if premerge
468 471 runs successfully. Use :forcedump to forcibly write files out.
469 472 """
470 473 a, b, c, back = files
471 474
472 475 fd = fcd.path()
473 476
474 477 util.copyfile(a, a + ".local")
475 478 repo.wwrite(fd + ".other", fco.data(), fco.flags())
476 479 repo.wwrite(fd + ".base", fca.data(), fca.flags())
477 480 return False, 1, False
478 481
479 482 @internaltool('forcedump', mergeonly)
480 483 def _forcedump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
481 484 labels=None):
482 485 """
483 486 Creates three versions of the files as same as :dump, but omits premerge.
484 487 """
485 488 return _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
486 489 labels=labels)
487 490
488 491 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
489 492 tool, toolpath, binary, symlink = toolconf
490 493 if fcd.isabsent() or fco.isabsent():
491 494 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
492 495 'for %s\n') % (tool, fcd.path()))
493 496 return False, 1, None
494 497 a, b, c, back = files
495 498 out = ""
496 499 env = {'HG_FILE': fcd.path(),
497 500 'HG_MY_NODE': short(mynode),
498 501 'HG_OTHER_NODE': str(fco.changectx()),
499 502 'HG_BASE_NODE': str(fca.changectx()),
500 503 'HG_MY_ISLINK': 'l' in fcd.flags(),
501 504 'HG_OTHER_ISLINK': 'l' in fco.flags(),
502 505 'HG_BASE_ISLINK': 'l' in fca.flags(),
503 506 }
504 507
505 508 ui = repo.ui
506 509
507 510 args = _toolstr(ui, tool, "args", '$local $base $other')
508 511 if "$output" in args:
509 512 out, a = a, back # read input from backup, write to original
510 513 replace = {'local': a, 'base': b, 'other': c, 'output': out}
511 514 args = util.interpolate(r'\$', replace, args,
512 515 lambda s: util.shellquote(util.localpath(s)))
513 516 cmd = toolpath + ' ' + args
514 517 if _toolbool(ui, tool, "gui"):
515 518 repo.ui.status(_('running merge tool %s for file %s\n') %
516 519 (tool, fcd.path()))
517 520 repo.ui.debug('launching merge tool: %s\n' % cmd)
518 521 r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool')
519 522 repo.ui.debug('merge tool returned: %s\n' % r)
520 523 return True, r, False
521 524
522 525 def _formatconflictmarker(repo, ctx, template, label, pad):
523 526 """Applies the given template to the ctx, prefixed by the label.
524 527
525 528 Pad is the minimum width of the label prefix, so that multiple markers
526 529 can have aligned templated parts.
527 530 """
528 531 if ctx.node() is None:
529 532 ctx = ctx.p1()
530 533
531 534 props = templatekw.keywords.copy()
532 535 props['templ'] = template
533 536 props['ctx'] = ctx
534 537 props['repo'] = repo
535 538 templateresult = template.render(props)
536 539
537 540 label = ('%s:' % label).ljust(pad + 1)
538 541 mark = '%s %s' % (label, templateresult)
539 542
540 543 if mark:
541 544 mark = mark.splitlines()[0] # split for safety
542 545
543 546 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
544 547 return util.ellipsis(mark, 80 - 8)
545 548
546 549 _defaultconflictlabels = ['local', 'other']
547 550
548 551 def _formatlabels(repo, fcd, fco, fca, labels):
549 552 """Formats the given labels using the conflict marker template.
550 553
551 554 Returns a list of formatted labels.
552 555 """
553 556 cd = fcd.changectx()
554 557 co = fco.changectx()
555 558 ca = fca.changectx()
556 559
557 560 ui = repo.ui
558 561 template = ui.config('ui', 'mergemarkertemplate')
559 562 template = templater.unquotestring(template)
560 563 tmpl = formatter.maketemplater(ui, template)
561 564
562 565 pad = max(len(l) for l in labels)
563 566
564 567 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
565 568 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
566 569 if len(labels) > 2:
567 570 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
568 571 return newlabels
569 572
570 573 def partextras(labels):
571 574 """Return a dictionary of extra labels for use in prompts to the user
572 575
573 576 Intended use is in strings of the form "(l)ocal%(l)s".
574 577 """
575 578 if labels is None:
576 579 return {
577 580 "l": "",
578 581 "o": "",
579 582 }
580 583
581 584 return {
582 585 "l": " [%s]" % labels[0],
583 586 "o": " [%s]" % labels[1],
584 587 }
585 588
586 589 def _filemerge(premerge, repo, mynode, orig, fcd, fco, fca, labels=None):
587 590 """perform a 3-way merge in the working directory
588 591
589 592 premerge = whether this is a premerge
590 593 mynode = parent node before merge
591 594 orig = original local filename before merge
592 595 fco = other file context
593 596 fca = ancestor file context
594 597 fcd = local file context for current/destination file
595 598
596 599 Returns whether the merge is complete, the return value of the merge, and
597 600 a boolean indicating whether the file was deleted from disk."""
598 601
599 602 def temp(prefix, ctx):
600 603 fullbase, ext = os.path.splitext(ctx.path())
601 604 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
602 605 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
603 606 data = repo.wwritedata(ctx.path(), ctx.data())
604 607 f = os.fdopen(fd, pycompat.sysstr("wb"))
605 608 f.write(data)
606 609 f.close()
607 610 return name
608 611
609 612 if not fco.cmp(fcd): # files identical?
610 613 return True, None, False
611 614
612 615 ui = repo.ui
613 616 fd = fcd.path()
614 617 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
615 618 symlink = 'l' in fcd.flags() + fco.flags()
616 619 changedelete = fcd.isabsent() or fco.isabsent()
617 620 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
618 621 if tool in internals and tool.startswith('internal:'):
619 622 # normalize to new-style names (':merge' etc)
620 623 tool = tool[len('internal'):]
621 624 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
622 625 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
623 626 pycompat.bytestr(changedelete)))
624 627
625 628 if tool in internals:
626 629 func = internals[tool]
627 630 mergetype = func.mergetype
628 631 onfailure = func.onfailure
629 632 precheck = func.precheck
630 633 else:
631 634 func = _xmerge
632 635 mergetype = fullmerge
633 636 onfailure = _("merging %s failed!\n")
634 637 precheck = None
635 638
636 639 toolconf = tool, toolpath, binary, symlink
637 640
638 641 if mergetype == nomerge:
639 642 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
640 643 return True, r, deleted
641 644
642 645 if premerge:
643 646 if orig != fco.path():
644 647 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
645 648 else:
646 649 ui.status(_("merging %s\n") % fd)
647 650
648 651 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
649 652
650 653 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
651 654 toolconf):
652 655 if onfailure:
653 656 ui.warn(onfailure % fd)
654 657 return True, 1, False
655 658
656 659 a = repo.wjoin(fd)
657 660 b = temp("base", fca)
658 661 c = temp("other", fco)
659 662 if not fcd.isabsent():
660 663 back = scmutil.origpath(ui, repo, a)
661 664 if premerge:
662 665 util.copyfile(a, back)
663 666 else:
664 667 back = None
665 668 files = (a, b, c, back)
666 669
667 670 r = 1
668 671 try:
669 672 markerstyle = ui.config('ui', 'mergemarkers')
670 673 if not labels:
671 674 labels = _defaultconflictlabels
672 675 if markerstyle != 'basic':
673 676 labels = _formatlabels(repo, fcd, fco, fca, labels)
674 677
675 678 if premerge and mergetype == fullmerge:
676 679 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
677 680 # complete if premerge successful (r is 0)
678 681 return not r, r, False
679 682
680 683 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
681 684 toolconf, files, labels=labels)
682 685
683 686 if needcheck:
684 687 r = _check(r, ui, tool, fcd, files)
685 688
686 689 if r:
687 690 if onfailure:
688 691 ui.warn(onfailure % fd)
689 692
690 693 return True, r, deleted
691 694 finally:
692 695 if not r and back is not None:
693 696 util.unlink(back)
694 697 util.unlink(b)
695 698 util.unlink(c)
696 699
697 700 def _check(r, ui, tool, fcd, files):
698 701 fd = fcd.path()
699 702 a, b, c, back = files
700 703
701 704 if not r and (_toolbool(ui, tool, "checkconflicts") or
702 705 'conflicts' in _toollist(ui, tool, "check")):
703 706 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
704 707 re.MULTILINE):
705 708 r = 1
706 709
707 710 checked = False
708 711 if 'prompt' in _toollist(ui, tool, "check"):
709 712 checked = True
710 713 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
711 714 "$$ &Yes $$ &No") % fd, 1):
712 715 r = 1
713 716
714 717 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
715 718 'changed' in
716 719 _toollist(ui, tool, "check")):
717 720 if back is not None and filecmp.cmp(a, back):
718 721 if ui.promptchoice(_(" output file %s appears unchanged\n"
719 722 "was merge successful (yn)?"
720 723 "$$ &Yes $$ &No") % fd, 1):
721 724 r = 1
722 725
723 726 if back is not None and _toolbool(ui, tool, "fixeol"):
724 727 _matcheol(a, back)
725 728
726 729 return r
727 730
728 731 def premerge(repo, mynode, orig, fcd, fco, fca, labels=None):
729 732 return _filemerge(True, repo, mynode, orig, fcd, fco, fca, labels=labels)
730 733
731 734 def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
732 735 return _filemerge(False, repo, mynode, orig, fcd, fco, fca, labels=labels)
733 736
734 737 def loadinternalmerge(ui, extname, registrarobj):
735 738 """Load internal merge tool from specified registrarobj
736 739 """
737 740 for name, func in registrarobj._table.iteritems():
738 741 fullname = ':' + name
739 742 internals[fullname] = func
740 743 internals['internal:' + name] = func
741 744 internalsdoc[fullname] = func
742 745
743 746 # load built-in merge tools explicitly to setup internalsdoc
744 747 loadinternalmerge(None, None, internaltool)
745 748
746 749 # tell hggettext to extract docstrings from these functions:
747 750 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now