##// END OF EJS Templates
filemerge: reduce creation of tempfiles until needed...
Phil Cohen -
r34037:96123bde default
parent child Browse files
Show More
@@ -1,774 +1,772 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 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 473 a = _workingpath(repo, fcd)
474 474 fd = fcd.path()
475 475
476 476 util.copyfile(a, a + ".local")
477 477 repo.wwrite(fd + ".other", fco.data(), fco.flags())
478 478 repo.wwrite(fd + ".base", fca.data(), fca.flags())
479 479 return False, 1, False
480 480
481 481 @internaltool('forcedump', mergeonly)
482 482 def _forcedump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
483 483 labels=None):
484 484 """
485 485 Creates three versions of the files as same as :dump, but omits premerge.
486 486 """
487 487 return _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
488 488 labels=labels)
489 489
490 490 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
491 491 tool, toolpath, binary, symlink = toolconf
492 492 if fcd.isabsent() or fco.isabsent():
493 493 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
494 494 'for %s\n') % (tool, fcd.path()))
495 495 return False, 1, None
496 unused, b, c, back = files
496 unused, unused, unused, back = files
497 497 a = _workingpath(repo, fcd)
498 out = ""
499 env = {'HG_FILE': fcd.path(),
500 'HG_MY_NODE': short(mynode),
501 'HG_OTHER_NODE': str(fco.changectx()),
502 'HG_BASE_NODE': str(fca.changectx()),
503 'HG_MY_ISLINK': 'l' in fcd.flags(),
504 'HG_OTHER_ISLINK': 'l' in fco.flags(),
505 'HG_BASE_ISLINK': 'l' in fca.flags(),
506 }
507
508 ui = repo.ui
498 b, c = _maketempfiles(repo, fco, fca)
499 try:
500 out = ""
501 env = {'HG_FILE': fcd.path(),
502 'HG_MY_NODE': short(mynode),
503 'HG_OTHER_NODE': str(fco.changectx()),
504 'HG_BASE_NODE': str(fca.changectx()),
505 'HG_MY_ISLINK': 'l' in fcd.flags(),
506 'HG_OTHER_ISLINK': 'l' in fco.flags(),
507 'HG_BASE_ISLINK': 'l' in fca.flags(),
508 }
509 ui = repo.ui
509 510
510 args = _toolstr(ui, tool, "args", '$local $base $other')
511 if "$output" in args:
512 out, a = a, back # read input from backup, write to original
513 replace = {'local': a, 'base': b, 'other': c, 'output': out}
514 args = util.interpolate(r'\$', replace, args,
515 lambda s: util.shellquote(util.localpath(s)))
516 cmd = toolpath + ' ' + args
517 if _toolbool(ui, tool, "gui"):
518 repo.ui.status(_('running merge tool %s for file %s\n') %
519 (tool, fcd.path()))
520 repo.ui.debug('launching merge tool: %s\n' % cmd)
521 r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool')
522 repo.ui.debug('merge tool returned: %s\n' % r)
523 return True, r, False
511 args = _toolstr(ui, tool, "args", '$local $base $other')
512 if "$output" in args:
513 out, a = a, back # read input from backup, write to original
514 replace = {'local': a, 'base': b, 'other': c, 'output': out}
515 args = util.interpolate(r'\$', replace, args,
516 lambda s: util.shellquote(util.localpath(s)))
517 cmd = toolpath + ' ' + args
518 if _toolbool(ui, tool, "gui"):
519 repo.ui.status(_('running merge tool %s for file %s\n') %
520 (tool, fcd.path()))
521 repo.ui.debug('launching merge tool: %s\n' % cmd)
522 r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool')
523 repo.ui.debug('merge tool returned: %s\n' % r)
524 return True, r, False
525 finally:
526 util.unlink(b)
527 util.unlink(c)
524 528
525 529 def _formatconflictmarker(repo, ctx, template, label, pad):
526 530 """Applies the given template to the ctx, prefixed by the label.
527 531
528 532 Pad is the minimum width of the label prefix, so that multiple markers
529 533 can have aligned templated parts.
530 534 """
531 535 if ctx.node() is None:
532 536 ctx = ctx.p1()
533 537
534 538 props = templatekw.keywords.copy()
535 539 props['templ'] = template
536 540 props['ctx'] = ctx
537 541 props['repo'] = repo
538 542 templateresult = template.render(props)
539 543
540 544 label = ('%s:' % label).ljust(pad + 1)
541 545 mark = '%s %s' % (label, templateresult)
542 546
543 547 if mark:
544 548 mark = mark.splitlines()[0] # split for safety
545 549
546 550 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
547 551 return util.ellipsis(mark, 80 - 8)
548 552
549 553 _defaultconflictlabels = ['local', 'other']
550 554
551 555 def _formatlabels(repo, fcd, fco, fca, labels):
552 556 """Formats the given labels using the conflict marker template.
553 557
554 558 Returns a list of formatted labels.
555 559 """
556 560 cd = fcd.changectx()
557 561 co = fco.changectx()
558 562 ca = fca.changectx()
559 563
560 564 ui = repo.ui
561 565 template = ui.config('ui', 'mergemarkertemplate')
562 566 template = templater.unquotestring(template)
563 567 tmpl = formatter.maketemplater(ui, template)
564 568
565 569 pad = max(len(l) for l in labels)
566 570
567 571 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
568 572 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
569 573 if len(labels) > 2:
570 574 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
571 575 return newlabels
572 576
573 577 def partextras(labels):
574 578 """Return a dictionary of extra labels for use in prompts to the user
575 579
576 580 Intended use is in strings of the form "(l)ocal%(l)s".
577 581 """
578 582 if labels is None:
579 583 return {
580 584 "l": "",
581 585 "o": "",
582 586 }
583 587
584 588 return {
585 589 "l": " [%s]" % labels[0],
586 590 "o": " [%s]" % labels[1],
587 591 }
588 592
589 593 def _makebackup(repo, ui, fcd, premerge):
590 594 """Makes a backup of the local `fcd` file prior to merging.
591 595
592 596 In addition to preserving the user's pre-existing modifications to `fcd`
593 597 (if any), the backup is used to undo certain premerges, confirm whether a
594 598 merge changed anything, and determine what line endings the new file should
595 599 have.
596 600 """
597 601 if fcd.isabsent():
598 602 return None
599 603
600 604 a = _workingpath(repo, fcd)
601 605 back = scmutil.origpath(ui, repo, a)
602 606 if premerge:
603 607 util.copyfile(a, back)
604 608 return back
605 609
606 def _maketempfiles(repo, fcd, fco, fca):
610 def _maketempfiles(repo, fco, fca):
607 611 """Writes out `fco` and `fca` as temporary files, so an external merge
608 612 tool may use them.
609
610 `fcd` is returned as-is, by convention, because it currently doubles as both
611 the local version and merge destination.
612 613 """
613 614 def temp(prefix, ctx):
614 615 fullbase, ext = os.path.splitext(ctx.path())
615 616 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
616 617 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
617 618 data = repo.wwritedata(ctx.path(), ctx.data())
618 619 f = os.fdopen(fd, pycompat.sysstr("wb"))
619 620 f.write(data)
620 621 f.close()
621 622 return name
622 623
623 a = repo.wjoin(fcd.path())
624 624 b = temp("base", fca)
625 625 c = temp("other", fco)
626 626
627 return a, b, c
627 return 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 files = _maketempfiles(repo, fcd, fco, fca) + (back,)
690 files = (None, None, None, 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 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 util.unlink(files[1])
719 util.unlink(files[2])
720 718
721 719 def _check(repo, r, ui, tool, fcd, files):
722 720 fd = fcd.path()
723 721 unused, 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(_workingpath(repo, fcd), 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(_workingpath(repo, fcd), back)
749 747
750 748 return r
751 749
752 750 def _workingpath(repo, ctx):
753 751 return repo.wjoin(ctx.path())
754 752
755 753 def premerge(repo, mynode, orig, fcd, fco, fca, labels=None):
756 754 return _filemerge(True, repo, mynode, orig, fcd, fco, fca, labels=labels)
757 755
758 756 def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
759 757 return _filemerge(False, repo, mynode, orig, fcd, fco, fca, labels=labels)
760 758
761 759 def loadinternalmerge(ui, extname, registrarobj):
762 760 """Load internal merge tool from specified registrarobj
763 761 """
764 762 for name, func in registrarobj._table.iteritems():
765 763 fullname = ':' + name
766 764 internals[fullname] = func
767 765 internals['internal:' + name] = func
768 766 internalsdoc[fullname] = func
769 767
770 768 # load built-in merge tools explicitly to setup internalsdoc
771 769 loadinternalmerge(None, None, internaltool)
772 770
773 771 # tell hggettext to extract docstrings from these functions:
774 772 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now