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