##// END OF EJS Templates
filemerge: do what the context __bytes__ does, but locally...
Augie Fackler -
r36442:3ab9d74d default
parent child Browse files
Show More
@@ -1,883 +1,883 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 mylabel, otherlabel = labels[:2]
517 517 if len(labels) >= 3:
518 518 baselabel = labels[2]
519 519 else:
520 520 baselabel = 'base'
521 521 env = {'HG_FILE': fcd.path(),
522 522 'HG_MY_NODE': short(mynode),
523 'HG_OTHER_NODE': str(fco.changectx()),
524 'HG_BASE_NODE': str(fca.changectx()),
523 'HG_OTHER_NODE': short(fco.changectx().node()),
524 'HG_BASE_NODE': short(fca.changectx().node()),
525 525 'HG_MY_ISLINK': 'l' in fcd.flags(),
526 526 'HG_OTHER_ISLINK': 'l' in fco.flags(),
527 527 'HG_BASE_ISLINK': 'l' in fca.flags(),
528 528 'HG_MY_LABEL': mylabel,
529 529 'HG_OTHER_LABEL': otherlabel,
530 530 'HG_BASE_LABEL': baselabel,
531 531 }
532 532 ui = repo.ui
533 533
534 534 args = _toolstr(ui, tool, "args")
535 535 if "$output" in args:
536 536 # read input from backup, write to original
537 537 out = a
538 538 a = repo.wvfs.join(back.path())
539 539 replace = {'local': a, 'base': b, 'other': c, 'output': out,
540 540 'labellocal': mylabel, 'labelother': otherlabel,
541 541 'labelbase': baselabel}
542 542 args = util.interpolate(br'\$', replace, args,
543 543 lambda s: util.shellquote(util.localpath(s)))
544 544 cmd = toolpath + ' ' + args
545 545 if _toolbool(ui, tool, "gui"):
546 546 repo.ui.status(_('running merge tool %s for file %s\n') %
547 547 (tool, fcd.path()))
548 548 repo.ui.debug('launching merge tool: %s\n' % cmd)
549 549 r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool')
550 550 repo.ui.debug('merge tool returned: %d\n' % r)
551 551 return True, r, False
552 552 finally:
553 553 util.unlink(b)
554 554 util.unlink(c)
555 555
556 556 def _formatconflictmarker(ctx, template, label, pad):
557 557 """Applies the given template to the ctx, prefixed by the label.
558 558
559 559 Pad is the minimum width of the label prefix, so that multiple markers
560 560 can have aligned templated parts.
561 561 """
562 562 if ctx.node() is None:
563 563 ctx = ctx.p1()
564 564
565 565 props = {'ctx': ctx}
566 566 templateresult = template.render(props)
567 567
568 568 label = ('%s:' % label).ljust(pad + 1)
569 569 mark = '%s %s' % (label, templateresult)
570 570
571 571 if mark:
572 572 mark = mark.splitlines()[0] # split for safety
573 573
574 574 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
575 575 return util.ellipsis(mark, 80 - 8)
576 576
577 577 _defaultconflictlabels = ['local', 'other']
578 578
579 579 def _formatlabels(repo, fcd, fco, fca, labels, tool=None):
580 580 """Formats the given labels using the conflict marker template.
581 581
582 582 Returns a list of formatted labels.
583 583 """
584 584 cd = fcd.changectx()
585 585 co = fco.changectx()
586 586 ca = fca.changectx()
587 587
588 588 ui = repo.ui
589 589 template = ui.config('ui', 'mergemarkertemplate')
590 590 if tool is not None:
591 591 template = _toolstr(ui, tool, 'mergemarkertemplate', template)
592 592 template = templater.unquotestring(template)
593 593 tres = formatter.templateresources(ui, repo)
594 594 tmpl = formatter.maketemplater(ui, template, defaults=templatekw.keywords,
595 595 resources=tres)
596 596
597 597 pad = max(len(l) for l in labels)
598 598
599 599 newlabels = [_formatconflictmarker(cd, tmpl, labels[0], pad),
600 600 _formatconflictmarker(co, tmpl, labels[1], pad)]
601 601 if len(labels) > 2:
602 602 newlabels.append(_formatconflictmarker(ca, tmpl, labels[2], pad))
603 603 return newlabels
604 604
605 605 def partextras(labels):
606 606 """Return a dictionary of extra labels for use in prompts to the user
607 607
608 608 Intended use is in strings of the form "(l)ocal%(l)s".
609 609 """
610 610 if labels is None:
611 611 return {
612 612 "l": "",
613 613 "o": "",
614 614 }
615 615
616 616 return {
617 617 "l": " [%s]" % labels[0],
618 618 "o": " [%s]" % labels[1],
619 619 }
620 620
621 621 def _restorebackup(fcd, back):
622 622 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
623 623 # util.copy here instead.
624 624 fcd.write(back.data(), fcd.flags())
625 625
626 626 def _makebackup(repo, ui, wctx, fcd, premerge):
627 627 """Makes and returns a filectx-like object for ``fcd``'s backup file.
628 628
629 629 In addition to preserving the user's pre-existing modifications to `fcd`
630 630 (if any), the backup is used to undo certain premerges, confirm whether a
631 631 merge changed anything, and determine what line endings the new file should
632 632 have.
633 633
634 634 Backups only need to be written once (right before the premerge) since their
635 635 content doesn't change afterwards.
636 636 """
637 637 if fcd.isabsent():
638 638 return None
639 639 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
640 640 # merge -> filemerge). (I suspect the fileset import is the weakest link)
641 641 from . import context
642 642 a = _workingpath(repo, fcd)
643 643 back = scmutil.origpath(ui, repo, a)
644 644 inworkingdir = (back.startswith(repo.wvfs.base) and not
645 645 back.startswith(repo.vfs.base))
646 646 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
647 647 # If the backup file is to be in the working directory, and we're
648 648 # merging in-memory, we must redirect the backup to the memory context
649 649 # so we don't disturb the working directory.
650 650 relpath = back[len(repo.wvfs.base) + 1:]
651 651 if premerge:
652 652 wctx[relpath].write(fcd.data(), fcd.flags())
653 653 return wctx[relpath]
654 654 else:
655 655 if premerge:
656 656 # Otherwise, write to wherever path the user specified the backups
657 657 # should go. We still need to switch based on whether the source is
658 658 # in-memory so we can use the fast path of ``util.copy`` if both are
659 659 # on disk.
660 660 if isinstance(fcd, context.overlayworkingfilectx):
661 661 util.writefile(back, fcd.data())
662 662 else:
663 663 util.copyfile(a, back)
664 664 # A arbitraryfilectx is returned, so we can run the same functions on
665 665 # the backup context regardless of where it lives.
666 666 return context.arbitraryfilectx(back, repo=repo)
667 667
668 668 def _maketempfiles(repo, fco, fca):
669 669 """Writes out `fco` and `fca` as temporary files, so an external merge
670 670 tool may use them.
671 671 """
672 672 def temp(prefix, ctx):
673 673 fullbase, ext = os.path.splitext(ctx.path())
674 674 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
675 675 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
676 676 data = repo.wwritedata(ctx.path(), ctx.data())
677 677 f = os.fdopen(fd, pycompat.sysstr("wb"))
678 678 f.write(data)
679 679 f.close()
680 680 return name
681 681
682 682 b = temp("base", fca)
683 683 c = temp("other", fco)
684 684
685 685 return b, c
686 686
687 687 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
688 688 """perform a 3-way merge in the working directory
689 689
690 690 premerge = whether this is a premerge
691 691 mynode = parent node before merge
692 692 orig = original local filename before merge
693 693 fco = other file context
694 694 fca = ancestor file context
695 695 fcd = local file context for current/destination file
696 696
697 697 Returns whether the merge is complete, the return value of the merge, and
698 698 a boolean indicating whether the file was deleted from disk."""
699 699
700 700 if not fco.cmp(fcd): # files identical?
701 701 return True, None, False
702 702
703 703 ui = repo.ui
704 704 fd = fcd.path()
705 705 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
706 706 symlink = 'l' in fcd.flags() + fco.flags()
707 707 changedelete = fcd.isabsent() or fco.isabsent()
708 708 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
709 709 if tool in internals and tool.startswith('internal:'):
710 710 # normalize to new-style names (':merge' etc)
711 711 tool = tool[len('internal'):]
712 712 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
713 713 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
714 714 pycompat.bytestr(changedelete)))
715 715
716 716 if tool in internals:
717 717 func = internals[tool]
718 718 mergetype = func.mergetype
719 719 onfailure = func.onfailure
720 720 precheck = func.precheck
721 721 isexternal = False
722 722 else:
723 723 if wctx.isinmemory():
724 724 func = _xmergeimm
725 725 else:
726 726 func = _xmerge
727 727 mergetype = fullmerge
728 728 onfailure = _("merging %s failed!\n")
729 729 precheck = None
730 730 isexternal = True
731 731
732 732 toolconf = tool, toolpath, binary, symlink
733 733
734 734 if mergetype == nomerge:
735 735 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
736 736 return True, r, deleted
737 737
738 738 if premerge:
739 739 if orig != fco.path():
740 740 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
741 741 else:
742 742 ui.status(_("merging %s\n") % fd)
743 743
744 744 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
745 745
746 746 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
747 747 toolconf):
748 748 if onfailure:
749 749 if wctx.isinmemory():
750 750 raise error.InMemoryMergeConflictsError('in-memory merge does '
751 751 'not support merge '
752 752 'conflicts')
753 753 ui.warn(onfailure % fd)
754 754 return True, 1, False
755 755
756 756 back = _makebackup(repo, ui, wctx, fcd, premerge)
757 757 files = (None, None, None, back)
758 758 r = 1
759 759 try:
760 760 internalmarkerstyle = ui.config('ui', 'mergemarkers')
761 761 if isexternal:
762 762 markerstyle = _toolstr(ui, tool, 'mergemarkers')
763 763 else:
764 764 markerstyle = internalmarkerstyle
765 765
766 766 if not labels:
767 767 labels = _defaultconflictlabels
768 768 formattedlabels = labels
769 769 if markerstyle != 'basic':
770 770 formattedlabels = _formatlabels(repo, fcd, fco, fca, labels,
771 771 tool=tool)
772 772
773 773 if premerge and mergetype == fullmerge:
774 774 # conflict markers generated by premerge will use 'detailed'
775 775 # settings if either ui.mergemarkers or the tool's mergemarkers
776 776 # setting is 'detailed'. This way tools can have basic labels in
777 777 # space-constrained areas of the UI, but still get full information
778 778 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
779 779 premergelabels = labels
780 780 labeltool = None
781 781 if markerstyle != 'basic':
782 782 # respect 'tool's mergemarkertemplate (which defaults to
783 783 # ui.mergemarkertemplate)
784 784 labeltool = tool
785 785 if internalmarkerstyle != 'basic' or markerstyle != 'basic':
786 786 premergelabels = _formatlabels(repo, fcd, fco, fca,
787 787 premergelabels, tool=labeltool)
788 788
789 789 r = _premerge(repo, fcd, fco, fca, toolconf, files,
790 790 labels=premergelabels)
791 791 # complete if premerge successful (r is 0)
792 792 return not r, r, False
793 793
794 794 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
795 795 toolconf, files, labels=formattedlabels)
796 796
797 797 if needcheck:
798 798 r = _check(repo, r, ui, tool, fcd, files)
799 799
800 800 if r:
801 801 if onfailure:
802 802 if wctx.isinmemory():
803 803 raise error.InMemoryMergeConflictsError('in-memory merge '
804 804 'does not support '
805 805 'merge conflicts')
806 806 ui.warn(onfailure % fd)
807 807 _onfilemergefailure(ui)
808 808
809 809 return True, r, deleted
810 810 finally:
811 811 if not r and back is not None:
812 812 back.remove()
813 813
814 814 def _haltmerge():
815 815 msg = _('merge halted after failed merge (see hg resolve)')
816 816 raise error.InterventionRequired(msg)
817 817
818 818 def _onfilemergefailure(ui):
819 819 action = ui.config('merge', 'on-failure')
820 820 if action == 'prompt':
821 821 msg = _('continue merge operation (yn)?' '$$ &Yes $$ &No')
822 822 if ui.promptchoice(msg, 0) == 1:
823 823 _haltmerge()
824 824 if action == 'halt':
825 825 _haltmerge()
826 826 # default action is 'continue', in which case we neither prompt nor halt
827 827
828 828 def _check(repo, r, ui, tool, fcd, files):
829 829 fd = fcd.path()
830 830 unused, unused, unused, back = files
831 831
832 832 if not r and (_toolbool(ui, tool, "checkconflicts") or
833 833 'conflicts' in _toollist(ui, tool, "check")):
834 834 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
835 835 re.MULTILINE):
836 836 r = 1
837 837
838 838 checked = False
839 839 if 'prompt' in _toollist(ui, tool, "check"):
840 840 checked = True
841 841 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
842 842 "$$ &Yes $$ &No") % fd, 1):
843 843 r = 1
844 844
845 845 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
846 846 'changed' in
847 847 _toollist(ui, tool, "check")):
848 848 if back is not None and not fcd.cmp(back):
849 849 if ui.promptchoice(_(" output file %s appears unchanged\n"
850 850 "was merge successful (yn)?"
851 851 "$$ &Yes $$ &No") % fd, 1):
852 852 r = 1
853 853
854 854 if back is not None and _toolbool(ui, tool, "fixeol"):
855 855 _matcheol(_workingpath(repo, fcd), back)
856 856
857 857 return r
858 858
859 859 def _workingpath(repo, ctx):
860 860 return repo.wjoin(ctx.path())
861 861
862 862 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
863 863 return _filemerge(True, repo, wctx, mynode, orig, fcd, fco, fca,
864 864 labels=labels)
865 865
866 866 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
867 867 return _filemerge(False, repo, wctx, mynode, orig, fcd, fco, fca,
868 868 labels=labels)
869 869
870 870 def loadinternalmerge(ui, extname, registrarobj):
871 871 """Load internal merge tool from specified registrarobj
872 872 """
873 873 for name, func in registrarobj._table.iteritems():
874 874 fullname = ':' + name
875 875 internals[fullname] = func
876 876 internals['internal:' + name] = func
877 877 internalsdoc[fullname] = func
878 878
879 879 # load built-in merge tools explicitly to setup internalsdoc
880 880 loadinternalmerge(None, None, internaltool)
881 881
882 882 # tell hggettext to extract docstrings from these functions:
883 883 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now