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