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