##// END OF EJS Templates
filemerge: move removal of `.orig` extension on temp file close to context...
Martin von Zweigbergk -
r49634:b53f2f5a default
parent child Browse files
Show More
@@ -1,1247 +1,1246 b''
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 746 localoutputpath = None
747 747 if b"$output" in args:
748 748 localoutputpath = backup.path()
749 # Remove the .orig to make syntax-highlighting more likely.
750 if localoutputpath.endswith(b'.orig'):
751 localoutputpath, ext = os.path.splitext(localoutputpath)
749 752
750 753 with _maketempfiles(
751 754 fco,
752 755 fca,
753 756 localoutputpath,
754 757 ) as temppaths:
755 758 basepath, otherpath, localoutputpath = temppaths
756 759 outpath = b""
757 760
758 761 def format_label(input):
759 762 if input.label_detail:
760 763 return b'%s: %s' % (input.label, input.label_detail)
761 764 else:
762 765 return input.label
763 766
764 767 env = {
765 768 b'HG_FILE': fcd.path(),
766 769 b'HG_MY_NODE': short(mynode),
767 770 b'HG_OTHER_NODE': short(fco.changectx().node()),
768 771 b'HG_BASE_NODE': short(fca.changectx().node()),
769 772 b'HG_MY_ISLINK': b'l' in fcd.flags(),
770 773 b'HG_OTHER_ISLINK': b'l' in fco.flags(),
771 774 b'HG_BASE_ISLINK': b'l' in fca.flags(),
772 775 b'HG_MY_LABEL': format_label(local),
773 776 b'HG_OTHER_LABEL': format_label(other),
774 777 b'HG_BASE_LABEL': format_label(base),
775 778 }
776 779 ui = repo.ui
777 780
778 781 if b"$output" in args:
779 782 # read input from backup, write to original
780 783 outpath = localpath
781 784 localpath = localoutputpath
782 785 replace = {
783 786 b'local': localpath,
784 787 b'base': basepath,
785 788 b'other': otherpath,
786 789 b'output': outpath,
787 790 b'labellocal': format_label(local),
788 791 b'labelother': format_label(other),
789 792 b'labelbase': format_label(base),
790 793 }
791 794 args = util.interpolate(
792 795 br'\$',
793 796 replace,
794 797 args,
795 798 lambda s: procutil.shellquote(util.localpath(s)),
796 799 )
797 800 if _toolbool(ui, tool, b"gui"):
798 801 repo.ui.status(
799 802 _(b'running merge tool %s for file %s\n')
800 803 % (tool, uipathfn(fcd.path()))
801 804 )
802 805 if scriptfn is None:
803 806 cmd = toolpath + b' ' + args
804 807 repo.ui.debug(b'launching merge tool: %s\n' % cmd)
805 808 _describemerge(ui, repo, mynode, fcd, fca, fco, env, toolpath, args)
806 809 r = ui.system(
807 810 cmd, cwd=repo.root, environ=env, blockedtag=b'mergetool'
808 811 )
809 812 else:
810 813 repo.ui.debug(
811 814 b'launching python merge script: %s:%s\n' % (toolpath, scriptfn)
812 815 )
813 816 r = 0
814 817 try:
815 818 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
816 819 from . import extensions
817 820
818 821 mod = extensions.loadpath(toolpath, b'hgmerge.%s' % tool)
819 822 except Exception:
820 823 raise error.Abort(
821 824 _(b"loading python merge script failed: %s") % toolpath
822 825 )
823 826 mergefn = getattr(mod, scriptfn, None)
824 827 if mergefn is None:
825 828 raise error.Abort(
826 829 _(b"%s does not have function: %s") % (toolpath, scriptfn)
827 830 )
828 831 argslist = procutil.shellsplit(args)
829 832 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
830 833 from . import hook
831 834
832 835 ret, raised = hook.pythonhook(
833 836 ui, repo, b"merge", toolpath, mergefn, {b'args': argslist}, True
834 837 )
835 838 if raised:
836 839 r = 1
837 840 repo.ui.debug(b'merge tool returned: %d\n' % r)
838 841 return True, r, False
839 842
840 843
841 844 def _populate_label_detail(input, template):
842 845 """Applies the given template to the ctx and stores it in the input."""
843 846 ctx = input.fctx.changectx()
844 847 if ctx.node() is None:
845 848 ctx = ctx.p1()
846 849
847 850 props = {b'ctx': ctx}
848 851 templateresult = template.renderdefault(props)
849 852 input.label_detail = templateresult.splitlines()[0] # split for safety
850 853
851 854
852 855 def _populate_label_details(repo, inputs, tool=None):
853 856 """Populates the label details using the conflict marker template."""
854 857 ui = repo.ui
855 858 template = ui.config(b'command-templates', b'mergemarker')
856 859 if tool is not None:
857 860 template = _toolstr(ui, tool, b'mergemarkertemplate', template)
858 861 template = templater.unquotestring(template)
859 862 tres = formatter.templateresources(ui, repo)
860 863 tmpl = formatter.maketemplater(
861 864 ui, template, defaults=templatekw.keywords, resources=tres
862 865 )
863 866
864 867 for input in inputs:
865 868 _populate_label_detail(input, tmpl)
866 869
867 870
868 871 def partextras(labels):
869 872 """Return a dictionary of extra labels for use in prompts to the user
870 873
871 874 Intended use is in strings of the form "(l)ocal%(l)s".
872 875 """
873 876 if labels is None:
874 877 return {
875 878 b"l": b"",
876 879 b"o": b"",
877 880 }
878 881
879 882 return {
880 883 b"l": b" [%s]" % labels[0],
881 884 b"o": b" [%s]" % labels[1],
882 885 }
883 886
884 887
885 888 def _makebackup(repo, ui, fcd):
886 889 """Makes and returns a filectx-like object for ``fcd``'s backup file.
887 890
888 891 In addition to preserving the user's pre-existing modifications to `fcd`
889 892 (if any), the backup is used to undo certain premerges, confirm whether a
890 893 merge changed anything, and determine what line endings the new file should
891 894 have.
892 895
893 896 Backups only need to be written once since their content doesn't change
894 897 afterwards.
895 898 """
896 899 if fcd.isabsent():
897 900 return None
898 901 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
899 902 # merge -> filemerge). (I suspect the fileset import is the weakest link)
900 903 from . import context
901 904
902 905 if isinstance(fcd, context.overlayworkingfilectx):
903 906 # If we're merging in-memory, we're free to put the backup anywhere.
904 907 fd, backup = pycompat.mkstemp(b'hg-merge-backup')
905 908 with os.fdopen(fd, 'wb') as f:
906 909 f.write(fcd.data())
907 910 else:
908 911 backup = scmutil.backuppath(ui, repo, fcd.path())
909 912 a = _workingpath(repo, fcd)
910 913 util.copyfile(a, backup)
911 914
912 915 return context.arbitraryfilectx(backup, repo=repo)
913 916
914 917
915 918 @contextlib.contextmanager
916 919 def _maketempfiles(fco, fca, localpath):
917 920 """Writes out `fco` and `fca` as temporary files, and (if localpath is not
918 921 None) copies `localpath` to another temporary file, so an external merge
919 922 tool may use them.
920 923 """
921 924 tmproot = pycompat.mkdtemp(prefix=b'hgmerge-')
922 925
923 926 def maketempfrompath(prefix, path):
924 927 fullbase, ext = os.path.splitext(path)
925 928 pre = b"%s~%s" % (os.path.basename(fullbase), prefix)
926 929 name = os.path.join(tmproot, pre)
927 930 if ext:
928 931 name += ext
929 932 f = open(name, "wb")
930 933 return f, name
931 934
932 935 def tempfromcontext(prefix, ctx):
933 936 f, name = maketempfrompath(prefix, ctx.path())
934 937 data = ctx.decodeddata()
935 938 f.write(data)
936 939 f.close()
937 940 return name
938 941
939 942 b = tempfromcontext(b"base", fca)
940 943 c = tempfromcontext(b"other", fco)
941 944 d = localpath
942 945 if localpath is not None:
943 # We start off with this being the backup filename, so remove the .orig
944 # to make syntax-highlighting more likely.
945 if d.endswith(b'.orig'):
946 d, _ = os.path.splitext(d)
947 946 f, d = maketempfrompath(b"local", d)
948 947 with open(localpath, b'rb') as src:
949 948 f.write(src.read())
950 949 f.close()
951 950
952 951 try:
953 952 yield b, c, d
954 953 finally:
955 954 shutil.rmtree(tmproot)
956 955
957 956
958 957 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
959 958 """perform a 3-way merge in the working directory
960 959
961 960 mynode = parent node before merge
962 961 orig = original local filename before merge
963 962 fco = other file context
964 963 fca = ancestor file context
965 964 fcd = local file context for current/destination file
966 965
967 966 Returns whether the merge is complete, the return value of the merge, and
968 967 a boolean indicating whether the file was deleted from disk."""
969 968 ui = repo.ui
970 969 fd = fcd.path()
971 970 uipathfn = scmutil.getuipathfn(repo)
972 971 fduipath = uipathfn(fd)
973 972 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
974 973 symlink = b'l' in fcd.flags() + fco.flags()
975 974 changedelete = fcd.isabsent() or fco.isabsent()
976 975 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
977 976 scriptfn = None
978 977 if tool in internals and tool.startswith(b'internal:'):
979 978 # normalize to new-style names (':merge' etc)
980 979 tool = tool[len(b'internal') :]
981 980 if toolpath and toolpath.startswith(b'python:'):
982 981 invalidsyntax = False
983 982 if toolpath.count(b':') >= 2:
984 983 script, scriptfn = toolpath[7:].rsplit(b':', 1)
985 984 if not scriptfn:
986 985 invalidsyntax = True
987 986 # missing :callable can lead to spliting on windows drive letter
988 987 if b'\\' in scriptfn or b'/' in scriptfn:
989 988 invalidsyntax = True
990 989 else:
991 990 invalidsyntax = True
992 991 if invalidsyntax:
993 992 raise error.Abort(_(b"invalid 'python:' syntax: %s") % toolpath)
994 993 toolpath = script
995 994 ui.debug(
996 995 b"picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
997 996 % (
998 997 tool,
999 998 fduipath,
1000 999 pycompat.bytestr(binary),
1001 1000 pycompat.bytestr(symlink),
1002 1001 pycompat.bytestr(changedelete),
1003 1002 )
1004 1003 )
1005 1004
1006 1005 if tool in internals:
1007 1006 func = internals[tool]
1008 1007 mergetype = func.mergetype
1009 1008 onfailure = func.onfailure
1010 1009 precheck = func.precheck
1011 1010 isexternal = False
1012 1011 else:
1013 1012 if wctx.isinmemory():
1014 1013 func = _xmergeimm
1015 1014 else:
1016 1015 func = _xmerge
1017 1016 mergetype = fullmerge
1018 1017 onfailure = _(b"merging %s failed!\n")
1019 1018 precheck = None
1020 1019 isexternal = True
1021 1020
1022 1021 toolconf = tool, toolpath, binary, symlink, scriptfn
1023 1022
1024 1023 if not labels:
1025 1024 labels = [b'local', b'other']
1026 1025 if len(labels) < 3:
1027 1026 labels.append(b'base')
1028 1027 local = simplemerge.MergeInput(fcd, labels[0])
1029 1028 other = simplemerge.MergeInput(fco, labels[1])
1030 1029 base = simplemerge.MergeInput(fca, labels[2])
1031 1030 if mergetype == nomerge:
1032 1031 return func(
1033 1032 repo,
1034 1033 mynode,
1035 1034 local,
1036 1035 other,
1037 1036 base,
1038 1037 toolconf,
1039 1038 )
1040 1039
1041 1040 if orig != fco.path():
1042 1041 ui.status(
1043 1042 _(b"merging %s and %s to %s\n")
1044 1043 % (uipathfn(orig), uipathfn(fco.path()), fduipath)
1045 1044 )
1046 1045 else:
1047 1046 ui.status(_(b"merging %s\n") % fduipath)
1048 1047
1049 1048 ui.debug(b"my %s other %s ancestor %s\n" % (fcd, fco, fca))
1050 1049
1051 1050 if precheck and not precheck(repo, mynode, fcd, fco, fca, toolconf):
1052 1051 if onfailure:
1053 1052 if wctx.isinmemory():
1054 1053 raise error.InMemoryMergeConflictsError(
1055 1054 b'in-memory merge does not support merge conflicts'
1056 1055 )
1057 1056 ui.warn(onfailure % fduipath)
1058 1057 return 1, False
1059 1058
1060 1059 backup = _makebackup(repo, ui, fcd)
1061 1060 r = 1
1062 1061 try:
1063 1062 internalmarkerstyle = ui.config(b'ui', b'mergemarkers')
1064 1063 if isexternal:
1065 1064 markerstyle = _toolstr(ui, tool, b'mergemarkers')
1066 1065 else:
1067 1066 markerstyle = internalmarkerstyle
1068 1067
1069 1068 if mergetype == fullmerge:
1070 1069 # conflict markers generated by premerge will use 'detailed'
1071 1070 # settings if either ui.mergemarkers or the tool's mergemarkers
1072 1071 # setting is 'detailed'. This way tools can have basic labels in
1073 1072 # space-constrained areas of the UI, but still get full information
1074 1073 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
1075 1074 labeltool = None
1076 1075 if markerstyle != b'basic':
1077 1076 # respect 'tool's mergemarkertemplate (which defaults to
1078 1077 # command-templates.mergemarker)
1079 1078 labeltool = tool
1080 1079 if internalmarkerstyle != b'basic' or markerstyle != b'basic':
1081 1080 _populate_label_details(
1082 1081 repo, [local, other, base], tool=labeltool
1083 1082 )
1084 1083
1085 1084 r = _premerge(
1086 1085 repo,
1087 1086 local,
1088 1087 other,
1089 1088 base,
1090 1089 toolconf,
1091 1090 )
1092 1091 # we're done if premerge was successful (r is 0)
1093 1092 if not r:
1094 1093 return r, False
1095 1094
1096 1095 # Reset to basic labels
1097 1096 local.label_detail = None
1098 1097 other.label_detail = None
1099 1098 base.label_detail = None
1100 1099
1101 1100 if markerstyle != b'basic':
1102 1101 _populate_label_details(repo, [local, other, base], tool=tool)
1103 1102
1104 1103 needcheck, r, deleted = func(
1105 1104 repo,
1106 1105 mynode,
1107 1106 local,
1108 1107 other,
1109 1108 base,
1110 1109 toolconf,
1111 1110 backup,
1112 1111 )
1113 1112
1114 1113 if needcheck:
1115 1114 r = _check(repo, r, ui, tool, fcd, backup)
1116 1115
1117 1116 if r:
1118 1117 if onfailure:
1119 1118 if wctx.isinmemory():
1120 1119 raise error.InMemoryMergeConflictsError(
1121 1120 b'in-memory merge '
1122 1121 b'does not support '
1123 1122 b'merge conflicts'
1124 1123 )
1125 1124 ui.warn(onfailure % fduipath)
1126 1125 _onfilemergefailure(ui)
1127 1126
1128 1127 return r, deleted
1129 1128 finally:
1130 1129 if not r and backup is not None:
1131 1130 backup.remove()
1132 1131
1133 1132
1134 1133 def _haltmerge():
1135 1134 msg = _(b'merge halted after failed merge (see hg resolve)')
1136 1135 raise error.InterventionRequired(msg)
1137 1136
1138 1137
1139 1138 def _onfilemergefailure(ui):
1140 1139 action = ui.config(b'merge', b'on-failure')
1141 1140 if action == b'prompt':
1142 1141 msg = _(b'continue merge operation (yn)?$$ &Yes $$ &No')
1143 1142 if ui.promptchoice(msg, 0) == 1:
1144 1143 _haltmerge()
1145 1144 if action == b'halt':
1146 1145 _haltmerge()
1147 1146 # default action is 'continue', in which case we neither prompt nor halt
1148 1147
1149 1148
1150 1149 def hasconflictmarkers(data):
1151 1150 # Detect lines starting with a string of 7 identical characters from the
1152 1151 # subset Mercurial uses for conflict markers, followed by either the end of
1153 1152 # line or a space and some text. Note that using [<>=+|-]{7} would detect
1154 1153 # `<><><><><` as a conflict marker, which we don't want.
1155 1154 return bool(
1156 1155 re.search(
1157 1156 br"^([<>=+|-])\1{6}( .*)$",
1158 1157 data,
1159 1158 re.MULTILINE,
1160 1159 )
1161 1160 )
1162 1161
1163 1162
1164 1163 def _check(repo, r, ui, tool, fcd, backup):
1165 1164 fd = fcd.path()
1166 1165 uipathfn = scmutil.getuipathfn(repo)
1167 1166
1168 1167 if not r and (
1169 1168 _toolbool(ui, tool, b"checkconflicts")
1170 1169 or b'conflicts' in _toollist(ui, tool, b"check")
1171 1170 ):
1172 1171 if hasconflictmarkers(fcd.data()):
1173 1172 r = 1
1174 1173
1175 1174 checked = False
1176 1175 if b'prompt' in _toollist(ui, tool, b"check"):
1177 1176 checked = True
1178 1177 if ui.promptchoice(
1179 1178 _(b"was merge of '%s' successful (yn)?$$ &Yes $$ &No")
1180 1179 % uipathfn(fd),
1181 1180 1,
1182 1181 ):
1183 1182 r = 1
1184 1183
1185 1184 if (
1186 1185 not r
1187 1186 and not checked
1188 1187 and (
1189 1188 _toolbool(ui, tool, b"checkchanged")
1190 1189 or b'changed' in _toollist(ui, tool, b"check")
1191 1190 )
1192 1191 ):
1193 1192 if backup is not None and not fcd.cmp(backup):
1194 1193 if ui.promptchoice(
1195 1194 _(
1196 1195 b" output file %s appears unchanged\n"
1197 1196 b"was merge successful (yn)?"
1198 1197 b"$$ &Yes $$ &No"
1199 1198 )
1200 1199 % uipathfn(fd),
1201 1200 1,
1202 1201 ):
1203 1202 r = 1
1204 1203
1205 1204 if backup is not None and _toolbool(ui, tool, b"fixeol"):
1206 1205 _matcheol(_workingpath(repo, fcd), backup)
1207 1206
1208 1207 return r
1209 1208
1210 1209
1211 1210 def _workingpath(repo, ctx):
1212 1211 return repo.wjoin(ctx.path())
1213 1212
1214 1213
1215 1214 def loadinternalmerge(ui, extname, registrarobj):
1216 1215 """Load internal merge tool from specified registrarobj"""
1217 1216 for name, func in pycompat.iteritems(registrarobj._table):
1218 1217 fullname = b':' + name
1219 1218 internals[fullname] = func
1220 1219 internals[b'internal:' + name] = func
1221 1220 internalsdoc[fullname] = func
1222 1221
1223 1222 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1224 1223 if capabilities:
1225 1224 capdesc = b" (actual capabilities: %s)" % b', '.join(
1226 1225 capabilities
1227 1226 )
1228 1227 func.__doc__ = func.__doc__ + pycompat.sysstr(b"\n\n%s" % capdesc)
1229 1228
1230 1229 # to put i18n comments into hg.pot for automatically generated texts
1231 1230
1232 1231 # i18n: "binary" and "symlink" are keywords
1233 1232 # i18n: this text is added automatically
1234 1233 _(b" (actual capabilities: binary, symlink)")
1235 1234 # i18n: "binary" is keyword
1236 1235 # i18n: this text is added automatically
1237 1236 _(b" (actual capabilities: binary)")
1238 1237 # i18n: "symlink" is keyword
1239 1238 # i18n: this text is added automatically
1240 1239 _(b" (actual capabilities: symlink)")
1241 1240
1242 1241
1243 1242 # load built-in merge tools explicitly to setup internalsdoc
1244 1243 loadinternalmerge(None, None, internaltool)
1245 1244
1246 1245 # tell hggettext to extract docstrings from these functions:
1247 1246 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now