##// END OF EJS Templates
simplemerge: use 3-way markers if mode=='merge3', ignoring number of labels...
Martin von Zweigbergk -
r49387:18a01dce default draft
parent child Browse files
Show More
@@ -1,102 +1,104
1 1 #!/usr/bin/env python3
2 2 from __future__ import absolute_import
3 3
4 4 import getopt
5 5 import sys
6 6
7 7 import hgdemandimport
8 8
9 9 hgdemandimport.enable()
10 10
11 11 from mercurial.i18n import _
12 12 from mercurial import (
13 13 context,
14 14 error,
15 15 fancyopts,
16 16 pycompat,
17 17 simplemerge,
18 18 ui as uimod,
19 19 )
20 20 from mercurial.utils import procutil, stringutil
21 21
22 22 options = [
23 23 (b'L', b'label', [], _(b'labels to use on conflict markers')),
24 24 (b'a', b'text', None, _(b'treat all files as text')),
25 25 (b'p', b'print', None, _(b'print results instead of overwriting LOCAL')),
26 26 (b'', b'no-minimal', None, _(b'no effect (DEPRECATED)')),
27 27 (b'h', b'help', None, _(b'display help and exit')),
28 28 (b'q', b'quiet', None, _(b'suppress output')),
29 29 ]
30 30
31 31 usage = _(
32 32 b'''simplemerge [OPTS] LOCAL BASE OTHER
33 33
34 34 Simple three-way file merge utility with a minimal feature set.
35 35
36 36 Apply to LOCAL the changes necessary to go from BASE to OTHER.
37 37
38 38 By default, LOCAL is overwritten with the results of this operation.
39 39 '''
40 40 )
41 41
42 42
43 43 class ParseError(Exception):
44 44 """Exception raised on errors in parsing the command line."""
45 45
46 46
47 47 def showhelp():
48 48 procutil.stdout.write(usage)
49 49 procutil.stdout.write(b'\noptions:\n')
50 50
51 51 out_opts = []
52 52 for shortopt, longopt, default, desc in options:
53 53 out_opts.append(
54 54 (
55 55 b'%2s%s'
56 56 % (
57 57 shortopt and b'-%s' % shortopt,
58 58 longopt and b' --%s' % longopt,
59 59 ),
60 60 b'%s' % desc,
61 61 )
62 62 )
63 63 opts_len = max([len(opt[0]) for opt in out_opts])
64 64 for first, second in out_opts:
65 65 procutil.stdout.write(b' %-*s %s\n' % (opts_len, first, second))
66 66
67 67
68 68 try:
69 69 for fp in (sys.stdin, procutil.stdout, sys.stderr):
70 70 procutil.setbinary(fp)
71 71
72 72 opts = {}
73 73 try:
74 74 bargv = [a.encode('utf8') for a in sys.argv[1:]]
75 75 args = fancyopts.fancyopts(bargv, options, opts)
76 76 except getopt.GetoptError as e:
77 77 raise ParseError(e)
78 78 if opts[b'help']:
79 79 showhelp()
80 80 sys.exit(0)
81 81 if len(args) != 3:
82 82 raise ParseError(_(b'wrong number of arguments').decode('utf8'))
83 if len(opts[b'label']) > 2:
84 opts[b'mode'] = b'merge3'
83 85 local, base, other = args
84 86 sys.exit(
85 87 simplemerge.simplemerge(
86 88 uimod.ui.load(),
87 89 context.arbitraryfilectx(local),
88 90 context.arbitraryfilectx(base),
89 91 context.arbitraryfilectx(other),
90 92 **pycompat.strkwargs(opts)
91 93 )
92 94 )
93 95 except ParseError as e:
94 96 e = stringutil.forcebytestr(e)
95 97 procutil.stdout.write(b"%s: %s\n" % (sys.argv[0].encode('utf8'), e))
96 98 showhelp()
97 99 sys.exit(1)
98 100 except error.Abort as e:
99 101 procutil.stderr.write(b"abort: %s\n" % e)
100 102 sys.exit(255)
101 103 except KeyboardInterrupt:
102 104 sys.exit(255)
@@ -1,1279 +1,1282
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, fcd, fco, fca, toolconf, labels=None):
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 = fcd.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 fcd.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(labels)
324 324 prompts[b'fd'] = uipathfn(fd)
325 325 try:
326 326 if fco.isabsent():
327 327 index = ui.promptchoice(_localchangedotherdeletedmsg % prompts, 2)
328 328 choice = [b'local', b'other', b'unresolved'][index]
329 329 elif fcd.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, fcd, fco, fca, toolconf, labels)
351 351 elif choice == b'local':
352 352 return _ilocal(repo, mynode, fcd, fco, fca, toolconf, labels)
353 353 elif choice == b'unresolved':
354 354 return _ifail(repo, mynode, fcd, fco, fca, toolconf, labels)
355 355 except error.ResponseExpected:
356 356 ui.write(b"\n")
357 357 return _ifail(repo, mynode, fcd, fco, fca, toolconf, labels)
358 358
359 359
360 360 @internaltool(b'local', nomerge)
361 361 def _ilocal(repo, mynode, fcd, fco, fca, toolconf, labels=None):
362 362 """Uses the local `p1()` version of files as the merged version."""
363 363 return 0, fcd.isabsent()
364 364
365 365
366 366 @internaltool(b'other', nomerge)
367 367 def _iother(repo, mynode, fcd, fco, fca, toolconf, labels=None):
368 368 """Uses the other `p2()` version of files as the merged version."""
369 369 if fco.isabsent():
370 370 # local changed, remote deleted -- 'deleted' picked
371 371 _underlyingfctxifabsent(fcd).remove()
372 372 deleted = True
373 373 else:
374 374 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
375 375 deleted = False
376 376 return 0, deleted
377 377
378 378
379 379 @internaltool(b'fail', nomerge)
380 380 def _ifail(repo, mynode, fcd, fco, fca, toolconf, labels=None):
381 381 """
382 382 Rather than attempting to merge files that were modified on both
383 383 branches, it marks them as unresolved. The resolve command must be
384 384 used to resolve these conflicts."""
385 385 # for change/delete conflicts write out the changed version, then fail
386 386 if fcd.isabsent():
387 387 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
388 388 return 1, False
389 389
390 390
391 391 def _underlyingfctxifabsent(filectx):
392 392 """Sometimes when resolving, our fcd is actually an absentfilectx, but
393 393 we want to write to it (to do the resolve). This helper returns the
394 394 underyling workingfilectx in that case.
395 395 """
396 396 if filectx.isabsent():
397 397 return filectx.changectx()[filectx.path()]
398 398 else:
399 399 return filectx
400 400
401 401
402 402 def _premerge(repo, fcd, fco, fca, toolconf, backup, labels=None):
403 403 tool, toolpath, binary, symlink, scriptfn = toolconf
404 404 if symlink or fcd.isabsent() or fco.isabsent():
405 405 return 1
406 406
407 407 ui = repo.ui
408 408
409 409 validkeep = [b'keep', b'keep-merge3', b'keep-mergediff']
410 410
411 411 # do we attempt to simplemerge first?
412 412 try:
413 413 premerge = _toolbool(ui, tool, b"premerge", not binary)
414 414 except error.ConfigError:
415 415 premerge = _toolstr(ui, tool, b"premerge", b"").lower()
416 416 if premerge not in validkeep:
417 417 _valid = b', '.join([b"'" + v + b"'" for v in validkeep])
418 418 raise error.ConfigError(
419 419 _(b"%s.premerge not valid ('%s' is neither boolean nor %s)")
420 420 % (tool, premerge, _valid)
421 421 )
422 422
423 423 if premerge:
424 if not labels:
425 labels = _defaultconflictlabels
426 if len(labels) < 3:
427 labels.append(b'base')
424 428 mode = b'merge'
425 if premerge in {b'keep-merge3', b'keep-mergediff'}:
426 if not labels:
427 labels = _defaultconflictlabels
428 if len(labels) < 3:
429 labels.append(b'base')
430 if premerge == b'keep-mergediff':
431 mode = b'mergediff'
429 if premerge == b'keep-mergediff':
430 mode = b'mergediff'
431 elif premerge == b'keep-merge3':
432 mode = b'merge3'
432 433 r = simplemerge.simplemerge(
433 434 ui, fcd, fca, fco, quiet=True, label=labels, mode=mode
434 435 )
435 436 if not r:
436 437 ui.debug(b" premerge successful\n")
437 438 return 0
438 439 if premerge not in validkeep:
439 440 # restore from backup and try again
440 441 _restorebackup(fcd, backup)
441 442 return 1 # continue merging
442 443
443 444
444 445 def _mergecheck(repo, mynode, fcd, fco, fca, toolconf):
445 446 tool, toolpath, binary, symlink, scriptfn = toolconf
446 447 uipathfn = scmutil.getuipathfn(repo)
447 448 if symlink:
448 449 repo.ui.warn(
449 450 _(b'warning: internal %s cannot merge symlinks for %s\n')
450 451 % (tool, uipathfn(fcd.path()))
451 452 )
452 453 return False
453 454 if fcd.isabsent() or fco.isabsent():
454 455 repo.ui.warn(
455 456 _(
456 457 b'warning: internal %s cannot merge change/delete '
457 458 b'conflict for %s\n'
458 459 )
459 460 % (tool, uipathfn(fcd.path()))
460 461 )
461 462 return False
462 463 return True
463 464
464 465
465 466 def _merge(repo, mynode, fcd, fco, fca, toolconf, backup, labels, mode):
466 467 """
467 468 Uses the internal non-interactive simple merge algorithm for merging
468 469 files. It will fail if there are any conflicts and leave markers in
469 470 the partially merged file. Markers will have two sections, one for each side
470 471 of merge, unless mode equals 'union' which suppresses the markers."""
471 472 ui = repo.ui
472 473
473 474 r = simplemerge.simplemerge(ui, fcd, fca, fco, label=labels, mode=mode)
474 475 return True, r, False
475 476
476 477
477 478 @internaltool(
478 479 b'union',
479 480 fullmerge,
480 481 _(
481 482 b"warning: conflicts while merging %s! "
482 483 b"(edit, then use 'hg resolve --mark')\n"
483 484 ),
484 485 precheck=_mergecheck,
485 486 )
486 487 def _iunion(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
487 488 """
488 489 Uses the internal non-interactive simple merge algorithm for merging
489 490 files. It will use both left and right sides for conflict regions.
490 491 No markers are inserted."""
491 492 return _merge(
492 493 repo, mynode, fcd, fco, fca, toolconf, backup, labels, b'union'
493 494 )
494 495
495 496
496 497 @internaltool(
497 498 b'merge',
498 499 fullmerge,
499 500 _(
500 501 b"warning: conflicts while merging %s! "
501 502 b"(edit, then use 'hg resolve --mark')\n"
502 503 ),
503 504 precheck=_mergecheck,
504 505 )
505 506 def _imerge(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
506 507 """
507 508 Uses the internal non-interactive simple merge algorithm for merging
508 509 files. It will fail if there are any conflicts and leave markers in
509 510 the partially merged file. Markers will have two sections, one for each side
510 511 of merge."""
511 512 return _merge(
512 513 repo, mynode, fcd, fco, fca, toolconf, backup, labels, b'merge'
513 514 )
514 515
515 516
516 517 @internaltool(
517 518 b'merge3',
518 519 fullmerge,
519 520 _(
520 521 b"warning: conflicts while merging %s! "
521 522 b"(edit, then use 'hg resolve --mark')\n"
522 523 ),
523 524 precheck=_mergecheck,
524 525 )
525 526 def _imerge3(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
526 527 """
527 528 Uses the internal non-interactive simple merge algorithm for merging
528 529 files. It will fail if there are any conflicts and leave markers in
529 530 the partially merged file. Marker will have three sections, one from each
530 531 side of the merge and one for the base content."""
531 532 if not labels:
532 533 labels = _defaultconflictlabels
533 534 if len(labels) < 3:
534 535 labels.append(b'base')
535 return _imerge(repo, mynode, fcd, fco, fca, toolconf, backup, labels)
536 return _merge(
537 repo, mynode, fcd, fco, fca, toolconf, backup, labels, b'merge3'
538 )
536 539
537 540
538 541 @internaltool(
539 542 b'merge3-lie-about-conflicts',
540 543 fullmerge,
541 544 b'',
542 545 precheck=_mergecheck,
543 546 )
544 547 def _imerge3alwaysgood(*args, **kwargs):
545 548 # Like merge3, but record conflicts as resolved with markers in place.
546 549 #
547 550 # This is used for `diff.merge` to show the differences between
548 551 # the auto-merge state and the committed merge state. It may be
549 552 # useful for other things.
550 553 b1, junk, b2 = _imerge3(*args, **kwargs)
551 554 # TODO is this right? I'm not sure what these return values mean,
552 555 # but as far as I can tell this will indicate to callers tha the
553 556 # merge succeeded.
554 557 return b1, False, b2
555 558
556 559
557 560 @internaltool(
558 561 b'mergediff',
559 562 fullmerge,
560 563 _(
561 564 b"warning: conflicts while merging %s! "
562 565 b"(edit, then use 'hg resolve --mark')\n"
563 566 ),
564 567 precheck=_mergecheck,
565 568 )
566 569 def _imerge_diff(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
567 570 """
568 571 Uses the internal non-interactive simple merge algorithm for merging
569 572 files. It will fail if there are any conflicts and leave markers in
570 573 the partially merged file. The marker will have two sections, one with the
571 574 content from one side of the merge, and one with a diff from the base
572 575 content to the content on the other side. (experimental)"""
573 576 if not labels:
574 577 labels = _defaultconflictlabels
575 578 if len(labels) < 3:
576 579 labels.append(b'base')
577 580 return _merge(
578 581 repo, mynode, fcd, fco, fca, toolconf, backup, labels, b'mergediff'
579 582 )
580 583
581 584
582 585 @internaltool(b'merge-local', mergeonly, precheck=_mergecheck)
583 586 def _imergelocal(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
584 587 """
585 588 Like :merge, but resolve all conflicts non-interactively in favor
586 589 of the local `p1()` changes."""
587 590 return _merge(
588 591 repo, mynode, fcd, fco, fca, toolconf, backup, labels, b'local'
589 592 )
590 593
591 594
592 595 @internaltool(b'merge-other', mergeonly, precheck=_mergecheck)
593 596 def _imergeother(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
594 597 """
595 598 Like :merge, but resolve all conflicts non-interactively in favor
596 599 of the other `p2()` changes."""
597 600 return _merge(
598 601 repo, mynode, fcd, fco, fca, toolconf, backup, labels, b'other'
599 602 )
600 603
601 604
602 605 @internaltool(
603 606 b'tagmerge',
604 607 mergeonly,
605 608 _(
606 609 b"automatic tag merging of %s failed! "
607 610 b"(use 'hg resolve --tool :merge' or another merge "
608 611 b"tool of your choice)\n"
609 612 ),
610 613 )
611 614 def _itagmerge(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
612 615 """
613 616 Uses the internal tag merge algorithm (experimental).
614 617 """
615 618 success, status = tagmerge.merge(repo, fcd, fco, fca)
616 619 return success, status, False
617 620
618 621
619 622 @internaltool(b'dump', fullmerge, binary=True, symlink=True)
620 623 def _idump(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
621 624 """
622 625 Creates three versions of the files to merge, containing the
623 626 contents of local, other and base. These files can then be used to
624 627 perform a merge manually. If the file to be merged is named
625 628 ``a.txt``, these files will accordingly be named ``a.txt.local``,
626 629 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
627 630 same directory as ``a.txt``.
628 631
629 632 This implies premerge. Therefore, files aren't dumped, if premerge
630 633 runs successfully. Use :forcedump to forcibly write files out.
631 634 """
632 635 a = _workingpath(repo, fcd)
633 636 fd = fcd.path()
634 637
635 638 from . import context
636 639
637 640 if isinstance(fcd, context.overlayworkingfilectx):
638 641 raise error.InMemoryMergeConflictsError(
639 642 b'in-memory merge does not support the :dump tool.'
640 643 )
641 644
642 645 util.writefile(a + b".local", fcd.decodeddata())
643 646 repo.wwrite(fd + b".other", fco.data(), fco.flags())
644 647 repo.wwrite(fd + b".base", fca.data(), fca.flags())
645 648 return False, 1, False
646 649
647 650
648 651 @internaltool(b'forcedump', mergeonly, binary=True, symlink=True)
649 652 def _forcedump(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
650 653 """
651 654 Creates three versions of the files as same as :dump, but omits premerge.
652 655 """
653 656 return _idump(repo, mynode, fcd, fco, fca, toolconf, backup, labels=labels)
654 657
655 658
656 659 def _xmergeimm(repo, mynode, fcd, fco, fca, toolconf, backup, labels=None):
657 660 # In-memory merge simply raises an exception on all external merge tools,
658 661 # for now.
659 662 #
660 663 # It would be possible to run most tools with temporary files, but this
661 664 # raises the question of what to do if the user only partially resolves the
662 665 # file -- we can't leave a merge state. (Copy to somewhere in the .hg/
663 666 # directory and tell the user how to get it is my best idea, but it's
664 667 # clunky.)
665 668 raise error.InMemoryMergeConflictsError(
666 669 b'in-memory merge does not support external merge tools'
667 670 )
668 671
669 672
670 673 def _describemerge(ui, repo, mynode, fcl, fcb, fco, env, toolpath, args):
671 674 tmpl = ui.config(b'command-templates', b'pre-merge-tool-output')
672 675 if not tmpl:
673 676 return
674 677
675 678 mappingdict = templateutil.mappingdict
676 679 props = {
677 680 b'ctx': fcl.changectx(),
678 681 b'node': hex(mynode),
679 682 b'path': fcl.path(),
680 683 b'local': mappingdict(
681 684 {
682 685 b'ctx': fcl.changectx(),
683 686 b'fctx': fcl,
684 687 b'node': hex(mynode),
685 688 b'name': _(b'local'),
686 689 b'islink': b'l' in fcl.flags(),
687 690 b'label': env[b'HG_MY_LABEL'],
688 691 }
689 692 ),
690 693 b'base': mappingdict(
691 694 {
692 695 b'ctx': fcb.changectx(),
693 696 b'fctx': fcb,
694 697 b'name': _(b'base'),
695 698 b'islink': b'l' in fcb.flags(),
696 699 b'label': env[b'HG_BASE_LABEL'],
697 700 }
698 701 ),
699 702 b'other': mappingdict(
700 703 {
701 704 b'ctx': fco.changectx(),
702 705 b'fctx': fco,
703 706 b'name': _(b'other'),
704 707 b'islink': b'l' in fco.flags(),
705 708 b'label': env[b'HG_OTHER_LABEL'],
706 709 }
707 710 ),
708 711 b'toolpath': toolpath,
709 712 b'toolargs': args,
710 713 }
711 714
712 715 # TODO: make all of this something that can be specified on a per-tool basis
713 716 tmpl = templater.unquotestring(tmpl)
714 717
715 718 # Not using cmdutil.rendertemplate here since it causes errors importing
716 719 # things for us to import cmdutil.
717 720 tres = formatter.templateresources(ui, repo)
718 721 t = formatter.maketemplater(
719 722 ui, tmpl, defaults=templatekw.keywords, resources=tres
720 723 )
721 724 ui.status(t.renderdefault(props))
722 725
723 726
724 727 def _xmerge(repo, mynode, fcd, fco, fca, toolconf, backup, labels):
725 728 tool, toolpath, binary, symlink, scriptfn = toolconf
726 729 uipathfn = scmutil.getuipathfn(repo)
727 730 if fcd.isabsent() or fco.isabsent():
728 731 repo.ui.warn(
729 732 _(b'warning: %s cannot merge change/delete conflict for %s\n')
730 733 % (tool, uipathfn(fcd.path()))
731 734 )
732 735 return False, 1, None
733 736 localpath = _workingpath(repo, fcd)
734 737 args = _toolstr(repo.ui, tool, b"args")
735 738
736 739 with _maketempfiles(
737 740 repo, fco, fca, repo.wvfs.join(backup.path()), b"$output" in args
738 741 ) as temppaths:
739 742 basepath, otherpath, localoutputpath = temppaths
740 743 outpath = b""
741 744 mylabel, otherlabel = labels[:2]
742 745 if len(labels) >= 3:
743 746 baselabel = labels[2]
744 747 else:
745 748 baselabel = b'base'
746 749 env = {
747 750 b'HG_FILE': fcd.path(),
748 751 b'HG_MY_NODE': short(mynode),
749 752 b'HG_OTHER_NODE': short(fco.changectx().node()),
750 753 b'HG_BASE_NODE': short(fca.changectx().node()),
751 754 b'HG_MY_ISLINK': b'l' in fcd.flags(),
752 755 b'HG_OTHER_ISLINK': b'l' in fco.flags(),
753 756 b'HG_BASE_ISLINK': b'l' in fca.flags(),
754 757 b'HG_MY_LABEL': mylabel,
755 758 b'HG_OTHER_LABEL': otherlabel,
756 759 b'HG_BASE_LABEL': baselabel,
757 760 }
758 761 ui = repo.ui
759 762
760 763 if b"$output" in args:
761 764 # read input from backup, write to original
762 765 outpath = localpath
763 766 localpath = localoutputpath
764 767 replace = {
765 768 b'local': localpath,
766 769 b'base': basepath,
767 770 b'other': otherpath,
768 771 b'output': outpath,
769 772 b'labellocal': mylabel,
770 773 b'labelother': otherlabel,
771 774 b'labelbase': baselabel,
772 775 }
773 776 args = util.interpolate(
774 777 br'\$',
775 778 replace,
776 779 args,
777 780 lambda s: procutil.shellquote(util.localpath(s)),
778 781 )
779 782 if _toolbool(ui, tool, b"gui"):
780 783 repo.ui.status(
781 784 _(b'running merge tool %s for file %s\n')
782 785 % (tool, uipathfn(fcd.path()))
783 786 )
784 787 if scriptfn is None:
785 788 cmd = toolpath + b' ' + args
786 789 repo.ui.debug(b'launching merge tool: %s\n' % cmd)
787 790 _describemerge(ui, repo, mynode, fcd, fca, fco, env, toolpath, args)
788 791 r = ui.system(
789 792 cmd, cwd=repo.root, environ=env, blockedtag=b'mergetool'
790 793 )
791 794 else:
792 795 repo.ui.debug(
793 796 b'launching python merge script: %s:%s\n' % (toolpath, scriptfn)
794 797 )
795 798 r = 0
796 799 try:
797 800 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
798 801 from . import extensions
799 802
800 803 mod = extensions.loadpath(toolpath, b'hgmerge.%s' % tool)
801 804 except Exception:
802 805 raise error.Abort(
803 806 _(b"loading python merge script failed: %s") % toolpath
804 807 )
805 808 mergefn = getattr(mod, scriptfn, None)
806 809 if mergefn is None:
807 810 raise error.Abort(
808 811 _(b"%s does not have function: %s") % (toolpath, scriptfn)
809 812 )
810 813 argslist = procutil.shellsplit(args)
811 814 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
812 815 from . import hook
813 816
814 817 ret, raised = hook.pythonhook(
815 818 ui, repo, b"merge", toolpath, mergefn, {b'args': argslist}, True
816 819 )
817 820 if raised:
818 821 r = 1
819 822 repo.ui.debug(b'merge tool returned: %d\n' % r)
820 823 return True, r, False
821 824
822 825
823 826 def _formatlabel(ctx, template, label, pad):
824 827 """Applies the given template to the ctx, prefixed by the label.
825 828
826 829 Pad is the minimum width of the label prefix, so that multiple markers
827 830 can have aligned templated parts.
828 831 """
829 832 if ctx.node() is None:
830 833 ctx = ctx.p1()
831 834
832 835 props = {b'ctx': ctx}
833 836 templateresult = template.renderdefault(props)
834 837
835 838 label = (b'%s:' % label).ljust(pad + 1)
836 839 mark = b'%s %s' % (label, templateresult)
837 840
838 841 if mark:
839 842 mark = mark.splitlines()[0] # split for safety
840 843
841 844 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
842 845 return stringutil.ellipsis(mark, 80 - 8)
843 846
844 847
845 848 _defaultconflictlabels = [b'local', b'other']
846 849
847 850
848 851 def _formatlabels(repo, fcd, fco, fca, labels, tool=None):
849 852 """Formats the given labels using the conflict marker template.
850 853
851 854 Returns a list of formatted labels.
852 855 """
853 856 cd = fcd.changectx()
854 857 co = fco.changectx()
855 858 ca = fca.changectx()
856 859
857 860 ui = repo.ui
858 861 template = ui.config(b'command-templates', b'mergemarker')
859 862 if tool is not None:
860 863 template = _toolstr(ui, tool, b'mergemarkertemplate', template)
861 864 template = templater.unquotestring(template)
862 865 tres = formatter.templateresources(ui, repo)
863 866 tmpl = formatter.maketemplater(
864 867 ui, template, defaults=templatekw.keywords, resources=tres
865 868 )
866 869
867 870 pad = max(len(l) for l in labels)
868 871
869 872 newlabels = [
870 873 _formatlabel(cd, tmpl, labels[0], pad),
871 874 _formatlabel(co, tmpl, labels[1], pad),
872 875 ]
873 876 if len(labels) > 2:
874 877 newlabels.append(_formatlabel(ca, tmpl, labels[2], pad))
875 878 return newlabels
876 879
877 880
878 881 def partextras(labels):
879 882 """Return a dictionary of extra labels for use in prompts to the user
880 883
881 884 Intended use is in strings of the form "(l)ocal%(l)s".
882 885 """
883 886 if labels is None:
884 887 return {
885 888 b"l": b"",
886 889 b"o": b"",
887 890 }
888 891
889 892 return {
890 893 b"l": b" [%s]" % labels[0],
891 894 b"o": b" [%s]" % labels[1],
892 895 }
893 896
894 897
895 898 def _restorebackup(fcd, backup):
896 899 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
897 900 # util.copy here instead.
898 901 fcd.write(backup.data(), fcd.flags())
899 902
900 903
901 904 def _makebackup(repo, ui, wctx, fcd):
902 905 """Makes and returns a filectx-like object for ``fcd``'s backup file.
903 906
904 907 In addition to preserving the user's pre-existing modifications to `fcd`
905 908 (if any), the backup is used to undo certain premerges, confirm whether a
906 909 merge changed anything, and determine what line endings the new file should
907 910 have.
908 911
909 912 Backups only need to be written once since their content doesn't change
910 913 afterwards.
911 914 """
912 915 if fcd.isabsent():
913 916 return None
914 917 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
915 918 # merge -> filemerge). (I suspect the fileset import is the weakest link)
916 919 from . import context
917 920
918 921 backup = scmutil.backuppath(ui, repo, fcd.path())
919 922 inworkingdir = backup.startswith(repo.wvfs.base) and not backup.startswith(
920 923 repo.vfs.base
921 924 )
922 925 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
923 926 # If the backup file is to be in the working directory, and we're
924 927 # merging in-memory, we must redirect the backup to the memory context
925 928 # so we don't disturb the working directory.
926 929 relpath = backup[len(repo.wvfs.base) + 1 :]
927 930 wctx[relpath].write(fcd.data(), fcd.flags())
928 931 return wctx[relpath]
929 932 else:
930 933 # Otherwise, write to wherever path the user specified the backups
931 934 # should go. We still need to switch based on whether the source is
932 935 # in-memory so we can use the fast path of ``util.copy`` if both are
933 936 # on disk.
934 937 if isinstance(fcd, context.overlayworkingfilectx):
935 938 util.writefile(backup, fcd.data())
936 939 else:
937 940 a = _workingpath(repo, fcd)
938 941 util.copyfile(a, backup)
939 942 # A arbitraryfilectx is returned, so we can run the same functions on
940 943 # the backup context regardless of where it lives.
941 944 return context.arbitraryfilectx(backup, repo=repo)
942 945
943 946
944 947 @contextlib.contextmanager
945 948 def _maketempfiles(repo, fco, fca, localpath, uselocalpath):
946 949 """Writes out `fco` and `fca` as temporary files, and (if uselocalpath)
947 950 copies `localpath` to another temporary file, so an external merge tool may
948 951 use them.
949 952 """
950 953 tmproot = None
951 954 tmprootprefix = repo.ui.config(b'experimental', b'mergetempdirprefix')
952 955 if tmprootprefix:
953 956 tmproot = pycompat.mkdtemp(prefix=tmprootprefix)
954 957
955 958 def maketempfrompath(prefix, path):
956 959 fullbase, ext = os.path.splitext(path)
957 960 pre = b"%s~%s" % (os.path.basename(fullbase), prefix)
958 961 if tmproot:
959 962 name = os.path.join(tmproot, pre)
960 963 if ext:
961 964 name += ext
962 965 f = open(name, "wb")
963 966 else:
964 967 fd, name = pycompat.mkstemp(prefix=pre + b'.', suffix=ext)
965 968 f = os.fdopen(fd, "wb")
966 969 return f, name
967 970
968 971 def tempfromcontext(prefix, ctx):
969 972 f, name = maketempfrompath(prefix, ctx.path())
970 973 data = ctx.decodeddata()
971 974 f.write(data)
972 975 f.close()
973 976 return name
974 977
975 978 b = tempfromcontext(b"base", fca)
976 979 c = tempfromcontext(b"other", fco)
977 980 d = localpath
978 981 if uselocalpath:
979 982 # We start off with this being the backup filename, so remove the .orig
980 983 # to make syntax-highlighting more likely.
981 984 if d.endswith(b'.orig'):
982 985 d, _ = os.path.splitext(d)
983 986 f, d = maketempfrompath(b"local", d)
984 987 with open(localpath, b'rb') as src:
985 988 f.write(src.read())
986 989 f.close()
987 990
988 991 try:
989 992 yield b, c, d
990 993 finally:
991 994 if tmproot:
992 995 shutil.rmtree(tmproot)
993 996 else:
994 997 util.unlink(b)
995 998 util.unlink(c)
996 999 # if not uselocalpath, d is the 'orig'/backup file which we
997 1000 # shouldn't delete.
998 1001 if d and uselocalpath:
999 1002 util.unlink(d)
1000 1003
1001 1004
1002 1005 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
1003 1006 """perform a 3-way merge in the working directory
1004 1007
1005 1008 mynode = parent node before merge
1006 1009 orig = original local filename before merge
1007 1010 fco = other file context
1008 1011 fca = ancestor file context
1009 1012 fcd = local file context for current/destination file
1010 1013
1011 1014 Returns whether the merge is complete, the return value of the merge, and
1012 1015 a boolean indicating whether the file was deleted from disk."""
1013 1016
1014 1017 if not fco.cmp(fcd): # files identical?
1015 1018 return None, False
1016 1019
1017 1020 ui = repo.ui
1018 1021 fd = fcd.path()
1019 1022 uipathfn = scmutil.getuipathfn(repo)
1020 1023 fduipath = uipathfn(fd)
1021 1024 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
1022 1025 symlink = b'l' in fcd.flags() + fco.flags()
1023 1026 changedelete = fcd.isabsent() or fco.isabsent()
1024 1027 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
1025 1028 scriptfn = None
1026 1029 if tool in internals and tool.startswith(b'internal:'):
1027 1030 # normalize to new-style names (':merge' etc)
1028 1031 tool = tool[len(b'internal') :]
1029 1032 if toolpath and toolpath.startswith(b'python:'):
1030 1033 invalidsyntax = False
1031 1034 if toolpath.count(b':') >= 2:
1032 1035 script, scriptfn = toolpath[7:].rsplit(b':', 1)
1033 1036 if not scriptfn:
1034 1037 invalidsyntax = True
1035 1038 # missing :callable can lead to spliting on windows drive letter
1036 1039 if b'\\' in scriptfn or b'/' in scriptfn:
1037 1040 invalidsyntax = True
1038 1041 else:
1039 1042 invalidsyntax = True
1040 1043 if invalidsyntax:
1041 1044 raise error.Abort(_(b"invalid 'python:' syntax: %s") % toolpath)
1042 1045 toolpath = script
1043 1046 ui.debug(
1044 1047 b"picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
1045 1048 % (
1046 1049 tool,
1047 1050 fduipath,
1048 1051 pycompat.bytestr(binary),
1049 1052 pycompat.bytestr(symlink),
1050 1053 pycompat.bytestr(changedelete),
1051 1054 )
1052 1055 )
1053 1056
1054 1057 if tool in internals:
1055 1058 func = internals[tool]
1056 1059 mergetype = func.mergetype
1057 1060 onfailure = func.onfailure
1058 1061 precheck = func.precheck
1059 1062 isexternal = False
1060 1063 else:
1061 1064 if wctx.isinmemory():
1062 1065 func = _xmergeimm
1063 1066 else:
1064 1067 func = _xmerge
1065 1068 mergetype = fullmerge
1066 1069 onfailure = _(b"merging %s failed!\n")
1067 1070 precheck = None
1068 1071 isexternal = True
1069 1072
1070 1073 toolconf = tool, toolpath, binary, symlink, scriptfn
1071 1074
1072 1075 if mergetype == nomerge:
1073 1076 return func(repo, mynode, fcd, fco, fca, toolconf, labels)
1074 1077
1075 1078 if orig != fco.path():
1076 1079 ui.status(
1077 1080 _(b"merging %s and %s to %s\n")
1078 1081 % (uipathfn(orig), uipathfn(fco.path()), fduipath)
1079 1082 )
1080 1083 else:
1081 1084 ui.status(_(b"merging %s\n") % fduipath)
1082 1085
1083 1086 ui.debug(b"my %s other %s ancestor %s\n" % (fcd, fco, fca))
1084 1087
1085 1088 if precheck and not precheck(repo, mynode, fcd, fco, fca, toolconf):
1086 1089 if onfailure:
1087 1090 if wctx.isinmemory():
1088 1091 raise error.InMemoryMergeConflictsError(
1089 1092 b'in-memory merge does not support merge conflicts'
1090 1093 )
1091 1094 ui.warn(onfailure % fduipath)
1092 1095 return 1, False
1093 1096
1094 1097 backup = _makebackup(repo, ui, wctx, fcd)
1095 1098 r = 1
1096 1099 try:
1097 1100 internalmarkerstyle = ui.config(b'ui', b'mergemarkers')
1098 1101 if isexternal:
1099 1102 markerstyle = _toolstr(ui, tool, b'mergemarkers')
1100 1103 else:
1101 1104 markerstyle = internalmarkerstyle
1102 1105
1103 1106 if not labels:
1104 1107 labels = _defaultconflictlabels
1105 1108 formattedlabels = labels
1106 1109 if markerstyle != b'basic':
1107 1110 formattedlabels = _formatlabels(
1108 1111 repo, fcd, fco, fca, labels, tool=tool
1109 1112 )
1110 1113
1111 1114 if mergetype == fullmerge:
1112 1115 # conflict markers generated by premerge will use 'detailed'
1113 1116 # settings if either ui.mergemarkers or the tool's mergemarkers
1114 1117 # setting is 'detailed'. This way tools can have basic labels in
1115 1118 # space-constrained areas of the UI, but still get full information
1116 1119 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
1117 1120 premergelabels = labels
1118 1121 labeltool = None
1119 1122 if markerstyle != b'basic':
1120 1123 # respect 'tool's mergemarkertemplate (which defaults to
1121 1124 # command-templates.mergemarker)
1122 1125 labeltool = tool
1123 1126 if internalmarkerstyle != b'basic' or markerstyle != b'basic':
1124 1127 premergelabels = _formatlabels(
1125 1128 repo, fcd, fco, fca, premergelabels, tool=labeltool
1126 1129 )
1127 1130
1128 1131 r = _premerge(
1129 1132 repo, fcd, fco, fca, toolconf, backup, labels=premergelabels
1130 1133 )
1131 1134 # we're done if premerge was successful (r is 0)
1132 1135 if not r:
1133 1136 return r, False
1134 1137
1135 1138 needcheck, r, deleted = func(
1136 1139 repo,
1137 1140 mynode,
1138 1141 fcd,
1139 1142 fco,
1140 1143 fca,
1141 1144 toolconf,
1142 1145 backup,
1143 1146 labels=formattedlabels,
1144 1147 )
1145 1148
1146 1149 if needcheck:
1147 1150 r = _check(repo, r, ui, tool, fcd, backup)
1148 1151
1149 1152 if r:
1150 1153 if onfailure:
1151 1154 if wctx.isinmemory():
1152 1155 raise error.InMemoryMergeConflictsError(
1153 1156 b'in-memory merge '
1154 1157 b'does not support '
1155 1158 b'merge conflicts'
1156 1159 )
1157 1160 ui.warn(onfailure % fduipath)
1158 1161 _onfilemergefailure(ui)
1159 1162
1160 1163 return r, deleted
1161 1164 finally:
1162 1165 if not r and backup is not None:
1163 1166 backup.remove()
1164 1167
1165 1168
1166 1169 def _haltmerge():
1167 1170 msg = _(b'merge halted after failed merge (see hg resolve)')
1168 1171 raise error.InterventionRequired(msg)
1169 1172
1170 1173
1171 1174 def _onfilemergefailure(ui):
1172 1175 action = ui.config(b'merge', b'on-failure')
1173 1176 if action == b'prompt':
1174 1177 msg = _(b'continue merge operation (yn)?$$ &Yes $$ &No')
1175 1178 if ui.promptchoice(msg, 0) == 1:
1176 1179 _haltmerge()
1177 1180 if action == b'halt':
1178 1181 _haltmerge()
1179 1182 # default action is 'continue', in which case we neither prompt nor halt
1180 1183
1181 1184
1182 1185 def hasconflictmarkers(data):
1183 1186 # Detect lines starting with a string of 7 identical characters from the
1184 1187 # subset Mercurial uses for conflict markers, followed by either the end of
1185 1188 # line or a space and some text. Note that using [<>=+|-]{7} would detect
1186 1189 # `<><><><><` as a conflict marker, which we don't want.
1187 1190 return bool(
1188 1191 re.search(
1189 1192 br"^([<>=+|-])\1{6}( .*)$",
1190 1193 data,
1191 1194 re.MULTILINE,
1192 1195 )
1193 1196 )
1194 1197
1195 1198
1196 1199 def _check(repo, r, ui, tool, fcd, backup):
1197 1200 fd = fcd.path()
1198 1201 uipathfn = scmutil.getuipathfn(repo)
1199 1202
1200 1203 if not r and (
1201 1204 _toolbool(ui, tool, b"checkconflicts")
1202 1205 or b'conflicts' in _toollist(ui, tool, b"check")
1203 1206 ):
1204 1207 if hasconflictmarkers(fcd.data()):
1205 1208 r = 1
1206 1209
1207 1210 checked = False
1208 1211 if b'prompt' in _toollist(ui, tool, b"check"):
1209 1212 checked = True
1210 1213 if ui.promptchoice(
1211 1214 _(b"was merge of '%s' successful (yn)?$$ &Yes $$ &No")
1212 1215 % uipathfn(fd),
1213 1216 1,
1214 1217 ):
1215 1218 r = 1
1216 1219
1217 1220 if (
1218 1221 not r
1219 1222 and not checked
1220 1223 and (
1221 1224 _toolbool(ui, tool, b"checkchanged")
1222 1225 or b'changed' in _toollist(ui, tool, b"check")
1223 1226 )
1224 1227 ):
1225 1228 if backup is not None and not fcd.cmp(backup):
1226 1229 if ui.promptchoice(
1227 1230 _(
1228 1231 b" output file %s appears unchanged\n"
1229 1232 b"was merge successful (yn)?"
1230 1233 b"$$ &Yes $$ &No"
1231 1234 )
1232 1235 % uipathfn(fd),
1233 1236 1,
1234 1237 ):
1235 1238 r = 1
1236 1239
1237 1240 if backup is not None and _toolbool(ui, tool, b"fixeol"):
1238 1241 _matcheol(_workingpath(repo, fcd), backup)
1239 1242
1240 1243 return r
1241 1244
1242 1245
1243 1246 def _workingpath(repo, ctx):
1244 1247 return repo.wjoin(ctx.path())
1245 1248
1246 1249
1247 1250 def loadinternalmerge(ui, extname, registrarobj):
1248 1251 """Load internal merge tool from specified registrarobj"""
1249 1252 for name, func in pycompat.iteritems(registrarobj._table):
1250 1253 fullname = b':' + name
1251 1254 internals[fullname] = func
1252 1255 internals[b'internal:' + name] = func
1253 1256 internalsdoc[fullname] = func
1254 1257
1255 1258 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1256 1259 if capabilities:
1257 1260 capdesc = b" (actual capabilities: %s)" % b', '.join(
1258 1261 capabilities
1259 1262 )
1260 1263 func.__doc__ = func.__doc__ + pycompat.sysstr(b"\n\n%s" % capdesc)
1261 1264
1262 1265 # to put i18n comments into hg.pot for automatically generated texts
1263 1266
1264 1267 # i18n: "binary" and "symlink" are keywords
1265 1268 # i18n: this text is added automatically
1266 1269 _(b" (actual capabilities: binary, symlink)")
1267 1270 # i18n: "binary" is keyword
1268 1271 # i18n: this text is added automatically
1269 1272 _(b" (actual capabilities: binary)")
1270 1273 # i18n: "symlink" is keyword
1271 1274 # i18n: this text is added automatically
1272 1275 _(b" (actual capabilities: symlink)")
1273 1276
1274 1277
1275 1278 # load built-in merge tools explicitly to setup internalsdoc
1276 1279 loadinternalmerge(None, None, internaltool)
1277 1280
1278 1281 # tell hggettext to extract docstrings from these functions:
1279 1282 i18nfunctions = internals.values()
@@ -1,527 +1,527
1 1 # Copyright (C) 2004, 2005 Canonical Ltd
2 2 #
3 3 # This program is free software; you can redistribute it and/or modify
4 4 # it under the terms of the GNU General Public License as published by
5 5 # the Free Software Foundation; either version 2 of the License, or
6 6 # (at your option) any later version.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU General Public License
14 14 # along with this program; if not, see <http://www.gnu.org/licenses/>.
15 15
16 16 # mbp: "you know that thing where cvs gives you conflict markers?"
17 17 # s: "i hate that."
18 18
19 19 from __future__ import absolute_import
20 20
21 21 from .i18n import _
22 22 from . import (
23 23 error,
24 24 mdiff,
25 25 pycompat,
26 26 )
27 27 from .utils import stringutil
28 28
29 29
30 30 class CantReprocessAndShowBase(Exception):
31 31 pass
32 32
33 33
34 34 def intersect(ra, rb):
35 35 """Given two ranges return the range where they intersect or None.
36 36
37 37 >>> intersect((0, 10), (0, 6))
38 38 (0, 6)
39 39 >>> intersect((0, 10), (5, 15))
40 40 (5, 10)
41 41 >>> intersect((0, 10), (10, 15))
42 42 >>> intersect((0, 9), (10, 15))
43 43 >>> intersect((0, 9), (7, 15))
44 44 (7, 9)
45 45 """
46 46 assert ra[0] <= ra[1]
47 47 assert rb[0] <= rb[1]
48 48
49 49 sa = max(ra[0], rb[0])
50 50 sb = min(ra[1], rb[1])
51 51 if sa < sb:
52 52 return sa, sb
53 53 else:
54 54 return None
55 55
56 56
57 57 def compare_range(a, astart, aend, b, bstart, bend):
58 58 """Compare a[astart:aend] == b[bstart:bend], without slicing."""
59 59 if (aend - astart) != (bend - bstart):
60 60 return False
61 61 for ia, ib in zip(
62 62 pycompat.xrange(astart, aend), pycompat.xrange(bstart, bend)
63 63 ):
64 64 if a[ia] != b[ib]:
65 65 return False
66 66 else:
67 67 return True
68 68
69 69
70 70 class Merge3Text(object):
71 71 """3-way merge of texts.
72 72
73 73 Given strings BASE, OTHER, THIS, tries to produce a combined text
74 74 incorporating the changes from both BASE->OTHER and BASE->THIS."""
75 75
76 76 def __init__(self, basetext, atext, btext, base=None, a=None, b=None):
77 77 self.basetext = basetext
78 78 self.atext = atext
79 79 self.btext = btext
80 80 if base is None:
81 81 base = mdiff.splitnewlines(basetext)
82 82 if a is None:
83 83 a = mdiff.splitnewlines(atext)
84 84 if b is None:
85 85 b = mdiff.splitnewlines(btext)
86 86 self.base = base
87 87 self.a = a
88 88 self.b = b
89 89
90 90 def merge_lines(
91 91 self,
92 92 name_a=None,
93 93 name_b=None,
94 94 name_base=None,
95 95 start_marker=b'<<<<<<<',
96 96 mid_marker=b'=======',
97 97 end_marker=b'>>>>>>>',
98 98 base_marker=None,
99 99 minimize=False,
100 100 ):
101 101 """Return merge in cvs-like form."""
102 102 self.conflicts = False
103 103 newline = b'\n'
104 104 if len(self.a) > 0:
105 105 if self.a[0].endswith(b'\r\n'):
106 106 newline = b'\r\n'
107 107 elif self.a[0].endswith(b'\r'):
108 108 newline = b'\r'
109 109 if name_a and start_marker:
110 110 start_marker = start_marker + b' ' + name_a
111 111 if name_b and end_marker:
112 112 end_marker = end_marker + b' ' + name_b
113 113 if name_base and base_marker:
114 114 base_marker = base_marker + b' ' + name_base
115 115 merge_groups = self.merge_groups()
116 116 if minimize:
117 117 merge_groups = self.minimize(merge_groups)
118 118 for what, lines in merge_groups:
119 119 if what == b'conflict':
120 120 base_lines, a_lines, b_lines = lines
121 121 self.conflicts = True
122 122 if start_marker is not None:
123 123 yield start_marker + newline
124 124 for line in a_lines:
125 125 yield line
126 126 if base_marker is not None:
127 127 yield base_marker + newline
128 128 for line in base_lines:
129 129 yield line
130 130 if mid_marker is not None:
131 131 yield mid_marker + newline
132 132 for line in b_lines:
133 133 yield line
134 134 if end_marker is not None:
135 135 yield end_marker + newline
136 136 else:
137 137 for line in lines:
138 138 yield line
139 139
140 140 def merge_groups(self):
141 141 """Yield sequence of line groups. Each one is a tuple:
142 142
143 143 'unchanged', lines
144 144 Lines unchanged from base
145 145
146 146 'a', lines
147 147 Lines taken from a
148 148
149 149 'same', lines
150 150 Lines taken from a (and equal to b)
151 151
152 152 'b', lines
153 153 Lines taken from b
154 154
155 155 'conflict', (base_lines, a_lines, b_lines)
156 156 Lines from base were changed to either a or b and conflict.
157 157 """
158 158 for t in self.merge_regions():
159 159 what = t[0]
160 160 if what == b'unchanged':
161 161 yield what, self.base[t[1] : t[2]]
162 162 elif what == b'a' or what == b'same':
163 163 yield what, self.a[t[1] : t[2]]
164 164 elif what == b'b':
165 165 yield what, self.b[t[1] : t[2]]
166 166 elif what == b'conflict':
167 167 yield (
168 168 what,
169 169 (
170 170 self.base[t[1] : t[2]],
171 171 self.a[t[3] : t[4]],
172 172 self.b[t[5] : t[6]],
173 173 ),
174 174 )
175 175 else:
176 176 raise ValueError(what)
177 177
178 178 def merge_regions(self):
179 179 """Return sequences of matching and conflicting regions.
180 180
181 181 This returns tuples, where the first value says what kind we
182 182 have:
183 183
184 184 'unchanged', start, end
185 185 Take a region of base[start:end]
186 186
187 187 'same', astart, aend
188 188 b and a are different from base but give the same result
189 189
190 190 'a', start, end
191 191 Non-clashing insertion from a[start:end]
192 192
193 193 'conflict', zstart, zend, astart, aend, bstart, bend
194 194 Conflict between a and b, with z as common ancestor
195 195
196 196 Method is as follows:
197 197
198 198 The two sequences align only on regions which match the base
199 199 and both descendants. These are found by doing a two-way diff
200 200 of each one against the base, and then finding the
201 201 intersections between those regions. These "sync regions"
202 202 are by definition unchanged in both and easily dealt with.
203 203
204 204 The regions in between can be in any of three cases:
205 205 conflicted, or changed on only one side.
206 206 """
207 207
208 208 # section a[0:ia] has been disposed of, etc
209 209 iz = ia = ib = 0
210 210
211 211 for region in self.find_sync_regions():
212 212 zmatch, zend, amatch, aend, bmatch, bend = region
213 213 # print 'match base [%d:%d]' % (zmatch, zend)
214 214
215 215 matchlen = zend - zmatch
216 216 assert matchlen >= 0
217 217 assert matchlen == (aend - amatch)
218 218 assert matchlen == (bend - bmatch)
219 219
220 220 len_a = amatch - ia
221 221 len_b = bmatch - ib
222 222 len_base = zmatch - iz
223 223 assert len_a >= 0
224 224 assert len_b >= 0
225 225 assert len_base >= 0
226 226
227 227 # print 'unmatched a=%d, b=%d' % (len_a, len_b)
228 228
229 229 if len_a or len_b:
230 230 # try to avoid actually slicing the lists
231 231 equal_a = compare_range(
232 232 self.a, ia, amatch, self.base, iz, zmatch
233 233 )
234 234 equal_b = compare_range(
235 235 self.b, ib, bmatch, self.base, iz, zmatch
236 236 )
237 237 same = compare_range(self.a, ia, amatch, self.b, ib, bmatch)
238 238
239 239 if same:
240 240 yield b'same', ia, amatch
241 241 elif equal_a and not equal_b:
242 242 yield b'b', ib, bmatch
243 243 elif equal_b and not equal_a:
244 244 yield b'a', ia, amatch
245 245 elif not equal_a and not equal_b:
246 246 yield b'conflict', iz, zmatch, ia, amatch, ib, bmatch
247 247 else:
248 248 raise AssertionError(b"can't handle a=b=base but unmatched")
249 249
250 250 ia = amatch
251 251 ib = bmatch
252 252 iz = zmatch
253 253
254 254 # if the same part of the base was deleted on both sides
255 255 # that's OK, we can just skip it.
256 256
257 257 if matchlen > 0:
258 258 assert ia == amatch
259 259 assert ib == bmatch
260 260 assert iz == zmatch
261 261
262 262 yield b'unchanged', zmatch, zend
263 263 iz = zend
264 264 ia = aend
265 265 ib = bend
266 266
267 267 def minimize(self, merge_groups):
268 268 """Trim conflict regions of lines where A and B sides match.
269 269
270 270 Lines where both A and B have made the same changes at the beginning
271 271 or the end of each merge region are eliminated from the conflict
272 272 region and are instead considered the same.
273 273 """
274 274 for what, lines in merge_groups:
275 275 if what != b"conflict":
276 276 yield what, lines
277 277 continue
278 278 base_lines, a_lines, b_lines = lines
279 279 alen = len(a_lines)
280 280 blen = len(b_lines)
281 281
282 282 # find matches at the front
283 283 ii = 0
284 284 while ii < alen and ii < blen and a_lines[ii] == b_lines[ii]:
285 285 ii += 1
286 286 startmatches = ii
287 287
288 288 # find matches at the end
289 289 ii = 0
290 290 while (
291 291 ii < alen and ii < blen and a_lines[-ii - 1] == b_lines[-ii - 1]
292 292 ):
293 293 ii += 1
294 294 endmatches = ii
295 295
296 296 if startmatches > 0:
297 297 yield b'same', a_lines[:startmatches]
298 298
299 299 yield (
300 300 b'conflict',
301 301 (
302 302 base_lines,
303 303 a_lines[startmatches : alen - endmatches],
304 304 b_lines[startmatches : blen - endmatches],
305 305 ),
306 306 )
307 307
308 308 if endmatches > 0:
309 309 yield b'same', a_lines[alen - endmatches :]
310 310
311 311 def find_sync_regions(self):
312 312 """Return a list of sync regions, where both descendants match the base.
313 313
314 314 Generates a list of (base1, base2, a1, a2, b1, b2). There is
315 315 always a zero-length sync region at the end of all the files.
316 316 """
317 317
318 318 ia = ib = 0
319 319 amatches = mdiff.get_matching_blocks(self.basetext, self.atext)
320 320 bmatches = mdiff.get_matching_blocks(self.basetext, self.btext)
321 321 len_a = len(amatches)
322 322 len_b = len(bmatches)
323 323
324 324 sl = []
325 325
326 326 while ia < len_a and ib < len_b:
327 327 abase, amatch, alen = amatches[ia]
328 328 bbase, bmatch, blen = bmatches[ib]
329 329
330 330 # there is an unconflicted block at i; how long does it
331 331 # extend? until whichever one ends earlier.
332 332 i = intersect((abase, abase + alen), (bbase, bbase + blen))
333 333 if i:
334 334 intbase = i[0]
335 335 intend = i[1]
336 336 intlen = intend - intbase
337 337
338 338 # found a match of base[i[0], i[1]]; this may be less than
339 339 # the region that matches in either one
340 340 assert intlen <= alen
341 341 assert intlen <= blen
342 342 assert abase <= intbase
343 343 assert bbase <= intbase
344 344
345 345 asub = amatch + (intbase - abase)
346 346 bsub = bmatch + (intbase - bbase)
347 347 aend = asub + intlen
348 348 bend = bsub + intlen
349 349
350 350 assert self.base[intbase:intend] == self.a[asub:aend], (
351 351 self.base[intbase:intend],
352 352 self.a[asub:aend],
353 353 )
354 354
355 355 assert self.base[intbase:intend] == self.b[bsub:bend]
356 356
357 357 sl.append((intbase, intend, asub, aend, bsub, bend))
358 358
359 359 # advance whichever one ends first in the base text
360 360 if (abase + alen) < (bbase + blen):
361 361 ia += 1
362 362 else:
363 363 ib += 1
364 364
365 365 intbase = len(self.base)
366 366 abase = len(self.a)
367 367 bbase = len(self.b)
368 368 sl.append((intbase, intbase, abase, abase, bbase, bbase))
369 369
370 370 return sl
371 371
372 372
373 373 def _verifytext(text, path, ui, opts):
374 374 """verifies that text is non-binary (unless opts[text] is passed,
375 375 then we just warn)"""
376 376 if stringutil.binary(text):
377 377 msg = _(b"%s looks like a binary file.") % path
378 378 if not opts.get('quiet'):
379 379 ui.warn(_(b'warning: %s\n') % msg)
380 380 if not opts.get('text'):
381 381 raise error.Abort(msg)
382 382 return text
383 383
384 384
385 385 def _picklabels(defaults, overrides):
386 386 if len(overrides) > 3:
387 387 raise error.Abort(_(b"can only specify three labels."))
388 388 result = defaults[:]
389 389 for i, override in enumerate(overrides):
390 390 result[i] = override
391 391 return result
392 392
393 393
394 394 def _mergediff(m3, name_a, name_b, name_base):
395 395 lines = []
396 396 conflicts = False
397 397 for what, group_lines in m3.merge_groups():
398 398 if what == b'conflict':
399 399 base_lines, a_lines, b_lines = group_lines
400 400 base_text = b''.join(base_lines)
401 401 b_blocks = list(
402 402 mdiff.allblocks(
403 403 base_text,
404 404 b''.join(b_lines),
405 405 lines1=base_lines,
406 406 lines2=b_lines,
407 407 )
408 408 )
409 409 a_blocks = list(
410 410 mdiff.allblocks(
411 411 base_text,
412 412 b''.join(a_lines),
413 413 lines1=base_lines,
414 414 lines2=b_lines,
415 415 )
416 416 )
417 417
418 418 def matching_lines(blocks):
419 419 return sum(
420 420 block[1] - block[0]
421 421 for block, kind in blocks
422 422 if kind == b'='
423 423 )
424 424
425 425 def diff_lines(blocks, lines1, lines2):
426 426 for block, kind in blocks:
427 427 if kind == b'=':
428 428 for line in lines1[block[0] : block[1]]:
429 429 yield b' ' + line
430 430 else:
431 431 for line in lines1[block[0] : block[1]]:
432 432 yield b'-' + line
433 433 for line in lines2[block[2] : block[3]]:
434 434 yield b'+' + line
435 435
436 436 lines.append(b"<<<<<<<\n")
437 437 if matching_lines(a_blocks) < matching_lines(b_blocks):
438 438 lines.append(b"======= %s\n" % name_a)
439 439 lines.extend(a_lines)
440 440 lines.append(b"------- %s\n" % name_base)
441 441 lines.append(b"+++++++ %s\n" % name_b)
442 442 lines.extend(diff_lines(b_blocks, base_lines, b_lines))
443 443 else:
444 444 lines.append(b"------- %s\n" % name_base)
445 445 lines.append(b"+++++++ %s\n" % name_a)
446 446 lines.extend(diff_lines(a_blocks, base_lines, a_lines))
447 447 lines.append(b"======= %s\n" % name_b)
448 448 lines.extend(b_lines)
449 449 lines.append(b">>>>>>>\n")
450 450 conflicts = True
451 451 else:
452 452 lines.extend(group_lines)
453 453 return lines, conflicts
454 454
455 455
456 456 def _resolve(m3, sides):
457 457 lines = []
458 458 for what, group_lines in m3.merge_groups():
459 459 if what == b'conflict':
460 460 for side in sides:
461 461 lines.extend(group_lines[side])
462 462 else:
463 463 lines.extend(group_lines)
464 464 return lines
465 465
466 466
467 467 def simplemerge(ui, localctx, basectx, otherctx, **opts):
468 468 """Performs the simplemerge algorithm.
469 469
470 470 The merged result is written into `localctx`.
471 471 """
472 472
473 473 def readctx(ctx):
474 474 # Merges were always run in the working copy before, which means
475 475 # they used decoded data, if the user defined any repository
476 476 # filters.
477 477 #
478 478 # Maintain that behavior today for BC, though perhaps in the future
479 479 # it'd be worth considering whether merging encoded data (what the
480 480 # repository usually sees) might be more useful.
481 481 return _verifytext(ctx.decodeddata(), ctx.path(), ui, opts)
482 482
483 483 try:
484 484 localtext = readctx(localctx)
485 485 basetext = readctx(basectx)
486 486 othertext = readctx(otherctx)
487 487 except error.Abort:
488 488 return 1
489 489
490 490 m3 = Merge3Text(basetext, localtext, othertext)
491 491 conflicts = False
492 492 mode = opts.get('mode', b'merge')
493 493 if mode == b'union':
494 494 lines = _resolve(m3, (1, 2))
495 495 elif mode == b'local':
496 496 lines = _resolve(m3, (1,))
497 497 elif mode == b'other':
498 498 lines = _resolve(m3, (2,))
499 499 else:
500 500 name_a, name_b, name_base = _picklabels(
501 501 [localctx.path(), otherctx.path(), None], opts.get('label', [])
502 502 )
503 503 if mode == b'mergediff':
504 504 lines, conflicts = _mergediff(m3, name_a, name_b, name_base)
505 505 else:
506 506 extrakwargs = {
507 507 'minimize': True,
508 508 }
509 if name_base is not None:
509 if mode == b'merge3':
510 510 extrakwargs['base_marker'] = b'|||||||'
511 511 extrakwargs['name_base'] = name_base
512 512 extrakwargs['minimize'] = False
513 513 lines = list(
514 514 m3.merge_lines(name_a=name_a, name_b=name_b, **extrakwargs)
515 515 )
516 516 conflicts = m3.conflicts
517 517
518 518 mergedtext = b''.join(lines)
519 519 if opts.get('print'):
520 520 ui.fout.write(mergedtext)
521 521 else:
522 522 # localctx.flags() already has the merged flags (done in
523 523 # mergestate.resolve())
524 524 localctx.write(mergedtext, localctx.flags())
525 525
526 526 if conflicts:
527 527 return 1
General Comments 0
You need to be logged in to leave comments. Login now