##// END OF EJS Templates
merge: respect ui.relative-paths...
Martin von Zweigbergk -
r41651:faa49a59 default
parent child Browse files
Show More
@@ -1,1058 +1,1065 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 contextlib
11 11 import os
12 12 import re
13 13 import shutil
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 hex,
18 18 nullid,
19 19 short,
20 20 )
21 21
22 22 from . import (
23 23 encoding,
24 24 error,
25 25 formatter,
26 26 match,
27 27 pycompat,
28 28 registrar,
29 29 scmutil,
30 30 simplemerge,
31 31 tagmerge,
32 32 templatekw,
33 33 templater,
34 34 templateutil,
35 35 util,
36 36 )
37 37
38 38 from .utils import (
39 39 procutil,
40 40 stringutil,
41 41 )
42 42
43 43 def _toolstr(ui, tool, part, *args):
44 44 return ui.config("merge-tools", tool + "." + part, *args)
45 45
46 46 def _toolbool(ui, tool, part,*args):
47 47 return ui.configbool("merge-tools", tool + "." + part, *args)
48 48
49 49 def _toollist(ui, tool, part):
50 50 return ui.configlist("merge-tools", tool + "." + part)
51 51
52 52 internals = {}
53 53 # Merge tools to document.
54 54 internalsdoc = {}
55 55
56 56 internaltool = registrar.internalmerge()
57 57
58 58 # internal tool merge types
59 59 nomerge = internaltool.nomerge
60 60 mergeonly = internaltool.mergeonly # just the full merge, no premerge
61 61 fullmerge = internaltool.fullmerge # both premerge and merge
62 62
63 63 _localchangedotherdeletedmsg = _(
64 64 "file '%(fd)s' was deleted in other%(o)s but was modified in local%(l)s.\n"
65 65 "What do you want to do?\n"
66 66 "use (c)hanged version, (d)elete, or leave (u)nresolved?"
67 67 "$$ &Changed $$ &Delete $$ &Unresolved")
68 68
69 69 _otherchangedlocaldeletedmsg = _(
70 70 "file '%(fd)s' was deleted in local%(l)s but was modified in other%(o)s.\n"
71 71 "What do you want to do?\n"
72 72 "use (c)hanged version, leave (d)eleted, or "
73 73 "leave (u)nresolved?"
74 74 "$$ &Changed $$ &Deleted $$ &Unresolved")
75 75
76 76 class absentfilectx(object):
77 77 """Represents a file that's ostensibly in a context but is actually not
78 78 present in it.
79 79
80 80 This is here because it's very specific to the filemerge code for now --
81 81 other code is likely going to break with the values this returns."""
82 82 def __init__(self, ctx, f):
83 83 self._ctx = ctx
84 84 self._f = f
85 85
86 86 def path(self):
87 87 return self._f
88 88
89 89 def size(self):
90 90 return None
91 91
92 92 def data(self):
93 93 return None
94 94
95 95 def filenode(self):
96 96 return nullid
97 97
98 98 _customcmp = True
99 99 def cmp(self, fctx):
100 100 """compare with other file context
101 101
102 102 returns True if different from fctx.
103 103 """
104 104 return not (fctx.isabsent() and
105 105 fctx.ctx() == self.ctx() and
106 106 fctx.path() == self.path())
107 107
108 108 def flags(self):
109 109 return ''
110 110
111 111 def changectx(self):
112 112 return self._ctx
113 113
114 114 def isbinary(self):
115 115 return False
116 116
117 117 def isabsent(self):
118 118 return True
119 119
120 120 def _findtool(ui, tool):
121 121 if tool in internals:
122 122 return tool
123 123 cmd = _toolstr(ui, tool, "executable", tool)
124 124 if cmd.startswith('python:'):
125 125 return cmd
126 126 return findexternaltool(ui, tool)
127 127
128 128 def _quotetoolpath(cmd):
129 129 if cmd.startswith('python:'):
130 130 return cmd
131 131 return procutil.shellquote(cmd)
132 132
133 133 def findexternaltool(ui, tool):
134 134 for kn in ("regkey", "regkeyalt"):
135 135 k = _toolstr(ui, tool, kn)
136 136 if not k:
137 137 continue
138 138 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
139 139 if p:
140 140 p = procutil.findexe(p + _toolstr(ui, tool, "regappend", ""))
141 141 if p:
142 142 return p
143 143 exe = _toolstr(ui, tool, "executable", tool)
144 144 return procutil.findexe(util.expandpath(exe))
145 145
146 146 def _picktool(repo, ui, path, binary, symlink, changedelete):
147 147 strictcheck = ui.configbool('merge', 'strict-capability-check')
148 148
149 149 def hascapability(tool, capability, strict=False):
150 150 if tool in internals:
151 151 return strict and internals[tool].capabilities.get(capability)
152 152 return _toolbool(ui, tool, capability)
153 153
154 154 def supportscd(tool):
155 155 return tool in internals and internals[tool].mergetype == nomerge
156 156
157 157 def check(tool, pat, symlink, binary, changedelete):
158 158 tmsg = tool
159 159 if pat:
160 160 tmsg = _("%s (for pattern %s)") % (tool, pat)
161 161 if not _findtool(ui, tool):
162 162 if pat: # explicitly requested tool deserves a warning
163 163 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
164 164 else: # configured but non-existing tools are more silent
165 165 ui.note(_("couldn't find merge tool %s\n") % tmsg)
166 166 elif symlink and not hascapability(tool, "symlink", strictcheck):
167 167 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
168 168 elif binary and not hascapability(tool, "binary", strictcheck):
169 169 ui.warn(_("tool %s can't handle binary\n") % tmsg)
170 170 elif changedelete and not supportscd(tool):
171 171 # the nomerge tools are the only tools that support change/delete
172 172 # conflicts
173 173 pass
174 174 elif not procutil.gui() and _toolbool(ui, tool, "gui"):
175 175 ui.warn(_("tool %s requires a GUI\n") % tmsg)
176 176 else:
177 177 return True
178 178 return False
179 179
180 180 # internal config: ui.forcemerge
181 181 # forcemerge comes from command line arguments, highest priority
182 182 force = ui.config('ui', 'forcemerge')
183 183 if force:
184 184 toolpath = _findtool(ui, force)
185 185 if changedelete and not supportscd(toolpath):
186 186 return ":prompt", None
187 187 else:
188 188 if toolpath:
189 189 return (force, _quotetoolpath(toolpath))
190 190 else:
191 191 # mimic HGMERGE if given tool not found
192 192 return (force, force)
193 193
194 194 # HGMERGE takes next precedence
195 195 hgmerge = encoding.environ.get("HGMERGE")
196 196 if hgmerge:
197 197 if changedelete and not supportscd(hgmerge):
198 198 return ":prompt", None
199 199 else:
200 200 return (hgmerge, hgmerge)
201 201
202 202 # then patterns
203 203
204 204 # whether binary capability should be checked strictly
205 205 binarycap = binary and strictcheck
206 206
207 207 for pat, tool in ui.configitems("merge-patterns"):
208 208 mf = match.match(repo.root, '', [pat])
209 209 if mf(path) and check(tool, pat, symlink, binarycap, changedelete):
210 210 if binary and not hascapability(tool, "binary", strict=True):
211 211 ui.warn(_("warning: check merge-patterns configurations,"
212 212 " if %r for binary file %r is unintentional\n"
213 213 "(see 'hg help merge-tools'"
214 214 " for binary files capability)\n")
215 215 % (pycompat.bytestr(tool), pycompat.bytestr(path)))
216 216 toolpath = _findtool(ui, tool)
217 217 return (tool, _quotetoolpath(toolpath))
218 218
219 219 # then merge tools
220 220 tools = {}
221 221 disabled = set()
222 222 for k, v in ui.configitems("merge-tools"):
223 223 t = k.split('.')[0]
224 224 if t not in tools:
225 225 tools[t] = int(_toolstr(ui, t, "priority"))
226 226 if _toolbool(ui, t, "disabled"):
227 227 disabled.add(t)
228 228 names = tools.keys()
229 229 tools = sorted([(-p, tool) for tool, p in tools.items()
230 230 if tool not in disabled])
231 231 uimerge = ui.config("ui", "merge")
232 232 if uimerge:
233 233 # external tools defined in uimerge won't be able to handle
234 234 # change/delete conflicts
235 235 if check(uimerge, path, symlink, binary, changedelete):
236 236 if uimerge not in names and not changedelete:
237 237 return (uimerge, uimerge)
238 238 tools.insert(0, (None, uimerge)) # highest priority
239 239 tools.append((None, "hgmerge")) # the old default, if found
240 240 for p, t in tools:
241 241 if check(t, None, symlink, binary, changedelete):
242 242 toolpath = _findtool(ui, t)
243 243 return (t, _quotetoolpath(toolpath))
244 244
245 245 # internal merge or prompt as last resort
246 246 if symlink or binary or changedelete:
247 247 if not changedelete and len(tools):
248 248 # any tool is rejected by capability for symlink or binary
249 249 ui.warn(_("no tool found to merge %s\n") % path)
250 250 return ":prompt", None
251 251 return ":merge", None
252 252
253 253 def _eoltype(data):
254 254 "Guess the EOL type of a file"
255 255 if '\0' in data: # binary
256 256 return None
257 257 if '\r\n' in data: # Windows
258 258 return '\r\n'
259 259 if '\r' in data: # Old Mac
260 260 return '\r'
261 261 if '\n' in data: # UNIX
262 262 return '\n'
263 263 return None # unknown
264 264
265 265 def _matcheol(file, back):
266 266 "Convert EOL markers in a file to match origfile"
267 267 tostyle = _eoltype(back.data()) # No repo.wread filters?
268 268 if tostyle:
269 269 data = util.readfile(file)
270 270 style = _eoltype(data)
271 271 if style:
272 272 newdata = data.replace(style, tostyle)
273 273 if newdata != data:
274 274 util.writefile(file, newdata)
275 275
276 276 @internaltool('prompt', nomerge)
277 277 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
278 278 """Asks the user which of the local `p1()` or the other `p2()` version to
279 279 keep as the merged version."""
280 280 ui = repo.ui
281 281 fd = fcd.path()
282 uipathfn = scmutil.getuipathfn(repo)
282 283
283 284 # Avoid prompting during an in-memory merge since it doesn't support merge
284 285 # conflicts.
285 286 if fcd.changectx().isinmemory():
286 287 raise error.InMemoryMergeConflictsError('in-memory merge does not '
287 288 'support file conflicts')
288 289
289 290 prompts = partextras(labels)
290 prompts['fd'] = fd
291 prompts['fd'] = uipathfn(fd)
291 292 try:
292 293 if fco.isabsent():
293 294 index = ui.promptchoice(
294 295 _localchangedotherdeletedmsg % prompts, 2)
295 296 choice = ['local', 'other', 'unresolved'][index]
296 297 elif fcd.isabsent():
297 298 index = ui.promptchoice(
298 299 _otherchangedlocaldeletedmsg % prompts, 2)
299 300 choice = ['other', 'local', 'unresolved'][index]
300 301 else:
301 302 index = ui.promptchoice(
302 303 _("keep (l)ocal%(l)s, take (o)ther%(o)s, or leave (u)nresolved"
303 304 " for %(fd)s?"
304 305 "$$ &Local $$ &Other $$ &Unresolved") % prompts, 2)
305 306 choice = ['local', 'other', 'unresolved'][index]
306 307
307 308 if choice == 'other':
308 309 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf,
309 310 labels)
310 311 elif choice == 'local':
311 312 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf,
312 313 labels)
313 314 elif choice == 'unresolved':
314 315 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
315 316 labels)
316 317 except error.ResponseExpected:
317 318 ui.write("\n")
318 319 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
319 320 labels)
320 321
321 322 @internaltool('local', nomerge)
322 323 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
323 324 """Uses the local `p1()` version of files as the merged version."""
324 325 return 0, fcd.isabsent()
325 326
326 327 @internaltool('other', nomerge)
327 328 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
328 329 """Uses the other `p2()` version of files as the merged version."""
329 330 if fco.isabsent():
330 331 # local changed, remote deleted -- 'deleted' picked
331 332 _underlyingfctxifabsent(fcd).remove()
332 333 deleted = True
333 334 else:
334 335 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
335 336 deleted = False
336 337 return 0, deleted
337 338
338 339 @internaltool('fail', nomerge)
339 340 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
340 341 """
341 342 Rather than attempting to merge files that were modified on both
342 343 branches, it marks them as unresolved. The resolve command must be
343 344 used to resolve these conflicts."""
344 345 # for change/delete conflicts write out the changed version, then fail
345 346 if fcd.isabsent():
346 347 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
347 348 return 1, False
348 349
349 350 def _underlyingfctxifabsent(filectx):
350 351 """Sometimes when resolving, our fcd is actually an absentfilectx, but
351 352 we want to write to it (to do the resolve). This helper returns the
352 353 underyling workingfilectx in that case.
353 354 """
354 355 if filectx.isabsent():
355 356 return filectx.changectx()[filectx.path()]
356 357 else:
357 358 return filectx
358 359
359 360 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
360 361 tool, toolpath, binary, symlink, scriptfn = toolconf
361 362 if symlink or fcd.isabsent() or fco.isabsent():
362 363 return 1
363 364 unused, unused, unused, back = files
364 365
365 366 ui = repo.ui
366 367
367 368 validkeep = ['keep', 'keep-merge3']
368 369
369 370 # do we attempt to simplemerge first?
370 371 try:
371 372 premerge = _toolbool(ui, tool, "premerge", not binary)
372 373 except error.ConfigError:
373 374 premerge = _toolstr(ui, tool, "premerge", "").lower()
374 375 if premerge not in validkeep:
375 376 _valid = ', '.join(["'" + v + "'" for v in validkeep])
376 377 raise error.ConfigError(_("%s.premerge not valid "
377 378 "('%s' is neither boolean nor %s)") %
378 379 (tool, premerge, _valid))
379 380
380 381 if premerge:
381 382 if premerge == 'keep-merge3':
382 383 if not labels:
383 384 labels = _defaultconflictlabels
384 385 if len(labels) < 3:
385 386 labels.append('base')
386 387 r = simplemerge.simplemerge(ui, fcd, fca, fco, quiet=True, label=labels)
387 388 if not r:
388 389 ui.debug(" premerge successful\n")
389 390 return 0
390 391 if premerge not in validkeep:
391 392 # restore from backup and try again
392 393 _restorebackup(fcd, back)
393 394 return 1 # continue merging
394 395
395 396 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
396 397 tool, toolpath, binary, symlink, scriptfn = toolconf
398 uipathfn = scmutil.getuipathfn(repo)
397 399 if symlink:
398 400 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
399 'for %s\n') % (tool, fcd.path()))
401 'for %s\n') % (tool, uipathfn(fcd.path())))
400 402 return False
401 403 if fcd.isabsent() or fco.isabsent():
402 404 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
403 'conflict for %s\n') % (tool, fcd.path()))
405 'conflict for %s\n') % (tool, uipathfn(fcd.path())))
404 406 return False
405 407 return True
406 408
407 409 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
408 410 """
409 411 Uses the internal non-interactive simple merge algorithm for merging
410 412 files. It will fail if there are any conflicts and leave markers in
411 413 the partially merged file. Markers will have two sections, one for each side
412 414 of merge, unless mode equals 'union' which suppresses the markers."""
413 415 ui = repo.ui
414 416
415 417 r = simplemerge.simplemerge(ui, fcd, fca, fco, label=labels, mode=mode)
416 418 return True, r, False
417 419
418 420 @internaltool('union', fullmerge,
419 421 _("warning: conflicts while merging %s! "
420 422 "(edit, then use 'hg resolve --mark')\n"),
421 423 precheck=_mergecheck)
422 424 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
423 425 """
424 426 Uses the internal non-interactive simple merge algorithm for merging
425 427 files. It will use both left and right sides for conflict regions.
426 428 No markers are inserted."""
427 429 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
428 430 files, labels, 'union')
429 431
430 432 @internaltool('merge', fullmerge,
431 433 _("warning: conflicts while merging %s! "
432 434 "(edit, then use 'hg resolve --mark')\n"),
433 435 precheck=_mergecheck)
434 436 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
435 437 """
436 438 Uses the internal non-interactive simple merge algorithm for merging
437 439 files. It will fail if there are any conflicts and leave markers in
438 440 the partially merged file. Markers will have two sections, one for each side
439 441 of merge."""
440 442 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
441 443 files, labels, 'merge')
442 444
443 445 @internaltool('merge3', fullmerge,
444 446 _("warning: conflicts while merging %s! "
445 447 "(edit, then use 'hg resolve --mark')\n"),
446 448 precheck=_mergecheck)
447 449 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
448 450 """
449 451 Uses the internal non-interactive simple merge algorithm for merging
450 452 files. It will fail if there are any conflicts and leave markers in
451 453 the partially merged file. Marker will have three sections, one from each
452 454 side of the merge and one for the base content."""
453 455 if not labels:
454 456 labels = _defaultconflictlabels
455 457 if len(labels) < 3:
456 458 labels.append('base')
457 459 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
458 460
459 461 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
460 462 labels=None, localorother=None):
461 463 """
462 464 Generic driver for _imergelocal and _imergeother
463 465 """
464 466 assert localorother is not None
465 467 r = simplemerge.simplemerge(repo.ui, fcd, fca, fco, label=labels,
466 468 localorother=localorother)
467 469 return True, r
468 470
469 471 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
470 472 def _imergelocal(*args, **kwargs):
471 473 """
472 474 Like :merge, but resolve all conflicts non-interactively in favor
473 475 of the local `p1()` changes."""
474 476 success, status = _imergeauto(localorother='local', *args, **kwargs)
475 477 return success, status, False
476 478
477 479 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
478 480 def _imergeother(*args, **kwargs):
479 481 """
480 482 Like :merge, but resolve all conflicts non-interactively in favor
481 483 of the other `p2()` changes."""
482 484 success, status = _imergeauto(localorother='other', *args, **kwargs)
483 485 return success, status, False
484 486
485 487 @internaltool('tagmerge', mergeonly,
486 488 _("automatic tag merging of %s failed! "
487 489 "(use 'hg resolve --tool :merge' or another merge "
488 490 "tool of your choice)\n"))
489 491 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
490 492 """
491 493 Uses the internal tag merge algorithm (experimental).
492 494 """
493 495 success, status = tagmerge.merge(repo, fcd, fco, fca)
494 496 return success, status, False
495 497
496 498 @internaltool('dump', fullmerge, binary=True, symlink=True)
497 499 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
498 500 """
499 501 Creates three versions of the files to merge, containing the
500 502 contents of local, other and base. These files can then be used to
501 503 perform a merge manually. If the file to be merged is named
502 504 ``a.txt``, these files will accordingly be named ``a.txt.local``,
503 505 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
504 506 same directory as ``a.txt``.
505 507
506 508 This implies premerge. Therefore, files aren't dumped, if premerge
507 509 runs successfully. Use :forcedump to forcibly write files out.
508 510 """
509 511 a = _workingpath(repo, fcd)
510 512 fd = fcd.path()
511 513
512 514 from . import context
513 515 if isinstance(fcd, context.overlayworkingfilectx):
514 516 raise error.InMemoryMergeConflictsError('in-memory merge does not '
515 517 'support the :dump tool.')
516 518
517 519 util.writefile(a + ".local", fcd.decodeddata())
518 520 repo.wwrite(fd + ".other", fco.data(), fco.flags())
519 521 repo.wwrite(fd + ".base", fca.data(), fca.flags())
520 522 return False, 1, False
521 523
522 524 @internaltool('forcedump', mergeonly, binary=True, symlink=True)
523 525 def _forcedump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
524 526 labels=None):
525 527 """
526 528 Creates three versions of the files as same as :dump, but omits premerge.
527 529 """
528 530 return _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
529 531 labels=labels)
530 532
531 533 def _xmergeimm(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
532 534 # In-memory merge simply raises an exception on all external merge tools,
533 535 # for now.
534 536 #
535 537 # It would be possible to run most tools with temporary files, but this
536 538 # raises the question of what to do if the user only partially resolves the
537 539 # file -- we can't leave a merge state. (Copy to somewhere in the .hg/
538 540 # directory and tell the user how to get it is my best idea, but it's
539 541 # clunky.)
540 542 raise error.InMemoryMergeConflictsError('in-memory merge does not support '
541 543 'external merge tools')
542 544
543 545 def _describemerge(ui, repo, mynode, fcl, fcb, fco, env, toolpath, args):
544 546 tmpl = ui.config('ui', 'pre-merge-tool-output-template')
545 547 if not tmpl:
546 548 return
547 549
548 550 mappingdict = templateutil.mappingdict
549 551 props = {'ctx': fcl.changectx(),
550 552 'node': hex(mynode),
551 553 'path': fcl.path(),
552 554 'local': mappingdict({'ctx': fcl.changectx(),
553 555 'fctx': fcl,
554 556 'node': hex(mynode),
555 557 'name': _('local'),
556 558 'islink': 'l' in fcl.flags(),
557 559 'label': env['HG_MY_LABEL']}),
558 560 'base': mappingdict({'ctx': fcb.changectx(),
559 561 'fctx': fcb,
560 562 'name': _('base'),
561 563 'islink': 'l' in fcb.flags(),
562 564 'label': env['HG_BASE_LABEL']}),
563 565 'other': mappingdict({'ctx': fco.changectx(),
564 566 'fctx': fco,
565 567 'name': _('other'),
566 568 'islink': 'l' in fco.flags(),
567 569 'label': env['HG_OTHER_LABEL']}),
568 570 'toolpath': toolpath,
569 571 'toolargs': args}
570 572
571 573 # TODO: make all of this something that can be specified on a per-tool basis
572 574 tmpl = templater.unquotestring(tmpl)
573 575
574 576 # Not using cmdutil.rendertemplate here since it causes errors importing
575 577 # things for us to import cmdutil.
576 578 tres = formatter.templateresources(ui, repo)
577 579 t = formatter.maketemplater(ui, tmpl, defaults=templatekw.keywords,
578 580 resources=tres)
579 581 ui.status(t.renderdefault(props))
580 582
581 583 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
582 584 tool, toolpath, binary, symlink, scriptfn = toolconf
585 uipathfn = scmutil.getuipathfn(repo)
583 586 if fcd.isabsent() or fco.isabsent():
584 587 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
585 'for %s\n') % (tool, fcd.path()))
588 'for %s\n') % (tool, uipathfn(fcd.path())))
586 589 return False, 1, None
587 590 unused, unused, unused, back = files
588 591 localpath = _workingpath(repo, fcd)
589 592 args = _toolstr(repo.ui, tool, "args")
590 593
591 594 with _maketempfiles(repo, fco, fca, repo.wvfs.join(back.path()),
592 595 "$output" in args) as temppaths:
593 596 basepath, otherpath, localoutputpath = temppaths
594 597 outpath = ""
595 598 mylabel, otherlabel = labels[:2]
596 599 if len(labels) >= 3:
597 600 baselabel = labels[2]
598 601 else:
599 602 baselabel = 'base'
600 603 env = {'HG_FILE': fcd.path(),
601 604 'HG_MY_NODE': short(mynode),
602 605 'HG_OTHER_NODE': short(fco.changectx().node()),
603 606 'HG_BASE_NODE': short(fca.changectx().node()),
604 607 'HG_MY_ISLINK': 'l' in fcd.flags(),
605 608 'HG_OTHER_ISLINK': 'l' in fco.flags(),
606 609 'HG_BASE_ISLINK': 'l' in fca.flags(),
607 610 'HG_MY_LABEL': mylabel,
608 611 'HG_OTHER_LABEL': otherlabel,
609 612 'HG_BASE_LABEL': baselabel,
610 613 }
611 614 ui = repo.ui
612 615
613 616 if "$output" in args:
614 617 # read input from backup, write to original
615 618 outpath = localpath
616 619 localpath = localoutputpath
617 620 replace = {'local': localpath, 'base': basepath, 'other': otherpath,
618 621 'output': outpath, 'labellocal': mylabel,
619 622 'labelother': otherlabel, 'labelbase': baselabel}
620 623 args = util.interpolate(
621 624 br'\$', replace, args,
622 625 lambda s: procutil.shellquote(util.localpath(s)))
623 626 if _toolbool(ui, tool, "gui"):
624 627 repo.ui.status(_('running merge tool %s for file %s\n') %
625 (tool, fcd.path()))
628 (tool, uipathfn(fcd.path())))
626 629 if scriptfn is None:
627 630 cmd = toolpath + ' ' + args
628 631 repo.ui.debug('launching merge tool: %s\n' % cmd)
629 632 _describemerge(ui, repo, mynode, fcd, fca, fco, env, toolpath, args)
630 633 r = ui.system(cmd, cwd=repo.root, environ=env,
631 634 blockedtag='mergetool')
632 635 else:
633 636 repo.ui.debug('launching python merge script: %s:%s\n' %
634 637 (toolpath, scriptfn))
635 638 r = 0
636 639 try:
637 640 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
638 641 from . import extensions
639 642 mod = extensions.loadpath(toolpath, 'hgmerge.%s' % tool)
640 643 except Exception:
641 644 raise error.Abort(_("loading python merge script failed: %s") %
642 645 toolpath)
643 646 mergefn = getattr(mod, scriptfn, None)
644 647 if mergefn is None:
645 648 raise error.Abort(_("%s does not have function: %s") %
646 649 (toolpath, scriptfn))
647 650 argslist = procutil.shellsplit(args)
648 651 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
649 652 from . import hook
650 653 ret, raised = hook.pythonhook(ui, repo, "merge", toolpath,
651 654 mergefn, {'args': argslist}, True)
652 655 if raised:
653 656 r = 1
654 657 repo.ui.debug('merge tool returned: %d\n' % r)
655 658 return True, r, False
656 659
657 660 def _formatconflictmarker(ctx, template, label, pad):
658 661 """Applies the given template to the ctx, prefixed by the label.
659 662
660 663 Pad is the minimum width of the label prefix, so that multiple markers
661 664 can have aligned templated parts.
662 665 """
663 666 if ctx.node() is None:
664 667 ctx = ctx.p1()
665 668
666 669 props = {'ctx': ctx}
667 670 templateresult = template.renderdefault(props)
668 671
669 672 label = ('%s:' % label).ljust(pad + 1)
670 673 mark = '%s %s' % (label, templateresult)
671 674
672 675 if mark:
673 676 mark = mark.splitlines()[0] # split for safety
674 677
675 678 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
676 679 return stringutil.ellipsis(mark, 80 - 8)
677 680
678 681 _defaultconflictlabels = ['local', 'other']
679 682
680 683 def _formatlabels(repo, fcd, fco, fca, labels, tool=None):
681 684 """Formats the given labels using the conflict marker template.
682 685
683 686 Returns a list of formatted labels.
684 687 """
685 688 cd = fcd.changectx()
686 689 co = fco.changectx()
687 690 ca = fca.changectx()
688 691
689 692 ui = repo.ui
690 693 template = ui.config('ui', 'mergemarkertemplate')
691 694 if tool is not None:
692 695 template = _toolstr(ui, tool, 'mergemarkertemplate', template)
693 696 template = templater.unquotestring(template)
694 697 tres = formatter.templateresources(ui, repo)
695 698 tmpl = formatter.maketemplater(ui, template, defaults=templatekw.keywords,
696 699 resources=tres)
697 700
698 701 pad = max(len(l) for l in labels)
699 702
700 703 newlabels = [_formatconflictmarker(cd, tmpl, labels[0], pad),
701 704 _formatconflictmarker(co, tmpl, labels[1], pad)]
702 705 if len(labels) > 2:
703 706 newlabels.append(_formatconflictmarker(ca, tmpl, labels[2], pad))
704 707 return newlabels
705 708
706 709 def partextras(labels):
707 710 """Return a dictionary of extra labels for use in prompts to the user
708 711
709 712 Intended use is in strings of the form "(l)ocal%(l)s".
710 713 """
711 714 if labels is None:
712 715 return {
713 716 "l": "",
714 717 "o": "",
715 718 }
716 719
717 720 return {
718 721 "l": " [%s]" % labels[0],
719 722 "o": " [%s]" % labels[1],
720 723 }
721 724
722 725 def _restorebackup(fcd, back):
723 726 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
724 727 # util.copy here instead.
725 728 fcd.write(back.data(), fcd.flags())
726 729
727 730 def _makebackup(repo, ui, wctx, fcd, premerge):
728 731 """Makes and returns a filectx-like object for ``fcd``'s backup file.
729 732
730 733 In addition to preserving the user's pre-existing modifications to `fcd`
731 734 (if any), the backup is used to undo certain premerges, confirm whether a
732 735 merge changed anything, and determine what line endings the new file should
733 736 have.
734 737
735 738 Backups only need to be written once (right before the premerge) since their
736 739 content doesn't change afterwards.
737 740 """
738 741 if fcd.isabsent():
739 742 return None
740 743 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
741 744 # merge -> filemerge). (I suspect the fileset import is the weakest link)
742 745 from . import context
743 746 a = _workingpath(repo, fcd)
744 747 back = scmutil.origpath(ui, repo, a)
745 748 inworkingdir = (back.startswith(repo.wvfs.base) and not
746 749 back.startswith(repo.vfs.base))
747 750 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
748 751 # If the backup file is to be in the working directory, and we're
749 752 # merging in-memory, we must redirect the backup to the memory context
750 753 # so we don't disturb the working directory.
751 754 relpath = back[len(repo.wvfs.base) + 1:]
752 755 if premerge:
753 756 wctx[relpath].write(fcd.data(), fcd.flags())
754 757 return wctx[relpath]
755 758 else:
756 759 if premerge:
757 760 # Otherwise, write to wherever path the user specified the backups
758 761 # should go. We still need to switch based on whether the source is
759 762 # in-memory so we can use the fast path of ``util.copy`` if both are
760 763 # on disk.
761 764 if isinstance(fcd, context.overlayworkingfilectx):
762 765 util.writefile(back, fcd.data())
763 766 else:
764 767 util.copyfile(a, back)
765 768 # A arbitraryfilectx is returned, so we can run the same functions on
766 769 # the backup context regardless of where it lives.
767 770 return context.arbitraryfilectx(back, repo=repo)
768 771
769 772 @contextlib.contextmanager
770 773 def _maketempfiles(repo, fco, fca, localpath, uselocalpath):
771 774 """Writes out `fco` and `fca` as temporary files, and (if uselocalpath)
772 775 copies `localpath` to another temporary file, so an external merge tool may
773 776 use them.
774 777 """
775 778 tmproot = None
776 779 tmprootprefix = repo.ui.config('experimental', 'mergetempdirprefix')
777 780 if tmprootprefix:
778 781 tmproot = pycompat.mkdtemp(prefix=tmprootprefix)
779 782
780 783 def maketempfrompath(prefix, path):
781 784 fullbase, ext = os.path.splitext(path)
782 785 pre = "%s~%s" % (os.path.basename(fullbase), prefix)
783 786 if tmproot:
784 787 name = os.path.join(tmproot, pre)
785 788 if ext:
786 789 name += ext
787 790 f = open(name, r"wb")
788 791 else:
789 792 fd, name = pycompat.mkstemp(prefix=pre + '.', suffix=ext)
790 793 f = os.fdopen(fd, r"wb")
791 794 return f, name
792 795
793 796 def tempfromcontext(prefix, ctx):
794 797 f, name = maketempfrompath(prefix, ctx.path())
795 798 data = repo.wwritedata(ctx.path(), ctx.data())
796 799 f.write(data)
797 800 f.close()
798 801 return name
799 802
800 803 b = tempfromcontext("base", fca)
801 804 c = tempfromcontext("other", fco)
802 805 d = localpath
803 806 if uselocalpath:
804 807 # We start off with this being the backup filename, so remove the .orig
805 808 # to make syntax-highlighting more likely.
806 809 if d.endswith('.orig'):
807 810 d, _ = os.path.splitext(d)
808 811 f, d = maketempfrompath("local", d)
809 812 with open(localpath, 'rb') as src:
810 813 f.write(src.read())
811 814 f.close()
812 815
813 816 try:
814 817 yield b, c, d
815 818 finally:
816 819 if tmproot:
817 820 shutil.rmtree(tmproot)
818 821 else:
819 822 util.unlink(b)
820 823 util.unlink(c)
821 824 # if not uselocalpath, d is the 'orig'/backup file which we
822 825 # shouldn't delete.
823 826 if d and uselocalpath:
824 827 util.unlink(d)
825 828
826 829 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
827 830 """perform a 3-way merge in the working directory
828 831
829 832 premerge = whether this is a premerge
830 833 mynode = parent node before merge
831 834 orig = original local filename before merge
832 835 fco = other file context
833 836 fca = ancestor file context
834 837 fcd = local file context for current/destination file
835 838
836 839 Returns whether the merge is complete, the return value of the merge, and
837 840 a boolean indicating whether the file was deleted from disk."""
838 841
839 842 if not fco.cmp(fcd): # files identical?
840 843 return True, None, False
841 844
842 845 ui = repo.ui
843 846 fd = fcd.path()
847 uipathfn = scmutil.getuipathfn(repo)
848 fduipath = uipathfn(fd)
844 849 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
845 850 symlink = 'l' in fcd.flags() + fco.flags()
846 851 changedelete = fcd.isabsent() or fco.isabsent()
847 852 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
848 853 scriptfn = None
849 854 if tool in internals and tool.startswith('internal:'):
850 855 # normalize to new-style names (':merge' etc)
851 856 tool = tool[len('internal'):]
852 857 if toolpath and toolpath.startswith('python:'):
853 858 invalidsyntax = False
854 859 if toolpath.count(':') >= 2:
855 860 script, scriptfn = toolpath[7:].rsplit(':', 1)
856 861 if not scriptfn:
857 862 invalidsyntax = True
858 863 # missing :callable can lead to spliting on windows drive letter
859 864 if '\\' in scriptfn or '/' in scriptfn:
860 865 invalidsyntax = True
861 866 else:
862 867 invalidsyntax = True
863 868 if invalidsyntax:
864 869 raise error.Abort(_("invalid 'python:' syntax: %s") % toolpath)
865 870 toolpath = script
866 871 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
867 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
868 pycompat.bytestr(changedelete)))
872 % (tool, fduipath, pycompat.bytestr(binary),
873 pycompat.bytestr(symlink), pycompat.bytestr(changedelete)))
869 874
870 875 if tool in internals:
871 876 func = internals[tool]
872 877 mergetype = func.mergetype
873 878 onfailure = func.onfailure
874 879 precheck = func.precheck
875 880 isexternal = False
876 881 else:
877 882 if wctx.isinmemory():
878 883 func = _xmergeimm
879 884 else:
880 885 func = _xmerge
881 886 mergetype = fullmerge
882 887 onfailure = _("merging %s failed!\n")
883 888 precheck = None
884 889 isexternal = True
885 890
886 891 toolconf = tool, toolpath, binary, symlink, scriptfn
887 892
888 893 if mergetype == nomerge:
889 894 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
890 895 return True, r, deleted
891 896
892 897 if premerge:
893 898 if orig != fco.path():
894 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
899 ui.status(_("merging %s and %s to %s\n") %
900 (uipathfn(orig), uipathfn(fco.path()), fduipath))
895 901 else:
896 ui.status(_("merging %s\n") % fd)
902 ui.status(_("merging %s\n") % fduipath)
897 903
898 904 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
899 905
900 906 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
901 907 toolconf):
902 908 if onfailure:
903 909 if wctx.isinmemory():
904 910 raise error.InMemoryMergeConflictsError('in-memory merge does '
905 911 'not support merge '
906 912 'conflicts')
907 ui.warn(onfailure % fd)
913 ui.warn(onfailure % fduipath)
908 914 return True, 1, False
909 915
910 916 back = _makebackup(repo, ui, wctx, fcd, premerge)
911 917 files = (None, None, None, back)
912 918 r = 1
913 919 try:
914 920 internalmarkerstyle = ui.config('ui', 'mergemarkers')
915 921 if isexternal:
916 922 markerstyle = _toolstr(ui, tool, 'mergemarkers')
917 923 else:
918 924 markerstyle = internalmarkerstyle
919 925
920 926 if not labels:
921 927 labels = _defaultconflictlabels
922 928 formattedlabels = labels
923 929 if markerstyle != 'basic':
924 930 formattedlabels = _formatlabels(repo, fcd, fco, fca, labels,
925 931 tool=tool)
926 932
927 933 if premerge and mergetype == fullmerge:
928 934 # conflict markers generated by premerge will use 'detailed'
929 935 # settings if either ui.mergemarkers or the tool's mergemarkers
930 936 # setting is 'detailed'. This way tools can have basic labels in
931 937 # space-constrained areas of the UI, but still get full information
932 938 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
933 939 premergelabels = labels
934 940 labeltool = None
935 941 if markerstyle != 'basic':
936 942 # respect 'tool's mergemarkertemplate (which defaults to
937 943 # ui.mergemarkertemplate)
938 944 labeltool = tool
939 945 if internalmarkerstyle != 'basic' or markerstyle != 'basic':
940 946 premergelabels = _formatlabels(repo, fcd, fco, fca,
941 947 premergelabels, tool=labeltool)
942 948
943 949 r = _premerge(repo, fcd, fco, fca, toolconf, files,
944 950 labels=premergelabels)
945 951 # complete if premerge successful (r is 0)
946 952 return not r, r, False
947 953
948 954 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
949 955 toolconf, files, labels=formattedlabels)
950 956
951 957 if needcheck:
952 958 r = _check(repo, r, ui, tool, fcd, files)
953 959
954 960 if r:
955 961 if onfailure:
956 962 if wctx.isinmemory():
957 963 raise error.InMemoryMergeConflictsError('in-memory merge '
958 964 'does not support '
959 965 'merge conflicts')
960 ui.warn(onfailure % fd)
966 ui.warn(onfailure % fduipath)
961 967 _onfilemergefailure(ui)
962 968
963 969 return True, r, deleted
964 970 finally:
965 971 if not r and back is not None:
966 972 back.remove()
967 973
968 974 def _haltmerge():
969 975 msg = _('merge halted after failed merge (see hg resolve)')
970 976 raise error.InterventionRequired(msg)
971 977
972 978 def _onfilemergefailure(ui):
973 979 action = ui.config('merge', 'on-failure')
974 980 if action == 'prompt':
975 981 msg = _('continue merge operation (yn)?' '$$ &Yes $$ &No')
976 982 if ui.promptchoice(msg, 0) == 1:
977 983 _haltmerge()
978 984 if action == 'halt':
979 985 _haltmerge()
980 986 # default action is 'continue', in which case we neither prompt nor halt
981 987
982 988 def hasconflictmarkers(data):
983 989 return bool(re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", data,
984 990 re.MULTILINE))
985 991
986 992 def _check(repo, r, ui, tool, fcd, files):
987 993 fd = fcd.path()
994 uipathfn = scmutil.getuipathfn(repo)
988 995 unused, unused, unused, back = files
989 996
990 997 if not r and (_toolbool(ui, tool, "checkconflicts") or
991 998 'conflicts' in _toollist(ui, tool, "check")):
992 999 if hasconflictmarkers(fcd.data()):
993 1000 r = 1
994 1001
995 1002 checked = False
996 1003 if 'prompt' in _toollist(ui, tool, "check"):
997 1004 checked = True
998 1005 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
999 "$$ &Yes $$ &No") % fd, 1):
1006 "$$ &Yes $$ &No") % uipathfn(fd), 1):
1000 1007 r = 1
1001 1008
1002 1009 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
1003 1010 'changed' in
1004 1011 _toollist(ui, tool, "check")):
1005 1012 if back is not None and not fcd.cmp(back):
1006 1013 if ui.promptchoice(_(" output file %s appears unchanged\n"
1007 1014 "was merge successful (yn)?"
1008 "$$ &Yes $$ &No") % fd, 1):
1015 "$$ &Yes $$ &No") % uipathfn(fd), 1):
1009 1016 r = 1
1010 1017
1011 1018 if back is not None and _toolbool(ui, tool, "fixeol"):
1012 1019 _matcheol(_workingpath(repo, fcd), back)
1013 1020
1014 1021 return r
1015 1022
1016 1023 def _workingpath(repo, ctx):
1017 1024 return repo.wjoin(ctx.path())
1018 1025
1019 1026 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
1020 1027 return _filemerge(True, repo, wctx, mynode, orig, fcd, fco, fca,
1021 1028 labels=labels)
1022 1029
1023 1030 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
1024 1031 return _filemerge(False, repo, wctx, mynode, orig, fcd, fco, fca,
1025 1032 labels=labels)
1026 1033
1027 1034 def loadinternalmerge(ui, extname, registrarobj):
1028 1035 """Load internal merge tool from specified registrarobj
1029 1036 """
1030 1037 for name, func in registrarobj._table.iteritems():
1031 1038 fullname = ':' + name
1032 1039 internals[fullname] = func
1033 1040 internals['internal:' + name] = func
1034 1041 internalsdoc[fullname] = func
1035 1042
1036 1043 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1037 1044 if capabilities:
1038 1045 capdesc = " (actual capabilities: %s)" % ', '.join(capabilities)
1039 1046 func.__doc__ = (func.__doc__ +
1040 1047 pycompat.sysstr("\n\n%s" % capdesc))
1041 1048
1042 1049 # to put i18n comments into hg.pot for automatically generated texts
1043 1050
1044 1051 # i18n: "binary" and "symlink" are keywords
1045 1052 # i18n: this text is added automatically
1046 1053 _(" (actual capabilities: binary, symlink)")
1047 1054 # i18n: "binary" is keyword
1048 1055 # i18n: this text is added automatically
1049 1056 _(" (actual capabilities: binary)")
1050 1057 # i18n: "symlink" is keyword
1051 1058 # i18n: this text is added automatically
1052 1059 _(" (actual capabilities: symlink)")
1053 1060
1054 1061 # load built-in merge tools explicitly to setup internalsdoc
1055 1062 loadinternalmerge(None, None, internaltool)
1056 1063
1057 1064 # tell hggettext to extract docstrings from these functions:
1058 1065 i18nfunctions = internals.values()
@@ -1,54 +1,55 b''
1 1 Test for changeset 9fe267f77f56ff127cf7e65dc15dd9de71ce8ceb
2 2 (merge correctly when all the files in a directory are moved
3 3 but then local changes are added in the same directory)
4 4
5 5 $ hg init a
6 6 $ cd a
7 7 $ mkdir -p testdir
8 8 $ echo a > testdir/a
9 9 $ hg add testdir/a
10 10 $ hg commit -m a
11 11 $ cd ..
12 12
13 13 $ hg clone a b
14 14 updating to branch default
15 15 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
16 16 $ cd a
17 17 $ echo alpha > testdir/a
18 18 $ hg commit -m remote-change
19 19 $ cd ..
20 20
21 21 $ cd b
22 22 $ mkdir testdir/subdir
23 23 $ hg mv testdir/a testdir/subdir/a
24 24 $ hg commit -m move
25 25 $ mkdir newdir
26 26 $ echo beta > newdir/beta
27 27 $ hg add newdir/beta
28 28 $ hg commit -m local-addition
29 29 $ hg pull ../a
30 30 pulling from ../a
31 31 searching for changes
32 32 adding changesets
33 33 adding manifests
34 34 adding file changes
35 35 added 1 changesets with 1 changes to 1 files (+1 heads)
36 36 new changesets cc7000b01af9
37 37 (run 'hg heads' to see heads, 'hg merge' to merge)
38 38 $ hg up -C 2
39 39 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
40 $ hg merge
41 merging testdir/subdir/a and testdir/a to testdir/subdir/a
40 Abuse this test for also testing that merge respects ui.relative-paths
41 $ hg --cwd testdir merge --config ui.relative-paths=yes
42 merging subdir/a and a to subdir/a
42 43 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
43 44 (branch merge, don't forget to commit)
44 45 $ hg stat
45 46 M testdir/subdir/a
46 47 $ hg diff --nodates
47 48 diff -r bc21c9773bfa testdir/subdir/a
48 49 --- a/testdir/subdir/a
49 50 +++ b/testdir/subdir/a
50 51 @@ -1,1 +1,1 @@
51 52 -a
52 53 +alpha
53 54
54 55 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now