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