##// END OF EJS Templates
simplemerge: stop accepting, and passing, file parameters...
Phil Cohen -
r33907:fa6309c5 default
parent child Browse files
Show More
@@ -1,104 +1,101 b''
1 1 #!/usr/bin/env python
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 hgdemandimport.enable()
9 9
10 10 from mercurial.i18n import _
11 11 from mercurial import (
12 12 error,
13 13 fancyopts,
14 14 simplemerge,
15 15 ui as uimod,
16 16 util,
17 17 )
18 18
19 19 options = [('L', 'label', [], _('labels to use on conflict markers')),
20 20 ('a', 'text', None, _('treat all files as text')),
21 21 ('p', 'print', None,
22 22 _('print results instead of overwriting LOCAL')),
23 23 ('', 'no-minimal', None, _('no effect (DEPRECATED)')),
24 24 ('h', 'help', None, _('display help and exit')),
25 25 ('q', 'quiet', None, _('suppress output'))]
26 26
27 27 usage = _('''simplemerge [OPTS] LOCAL BASE OTHER
28 28
29 29 Simple three-way file merge utility with a minimal feature set.
30 30
31 31 Apply to LOCAL the changes necessary to go from BASE to OTHER.
32 32
33 33 By default, LOCAL is overwritten with the results of this operation.
34 34 ''')
35 35
36 36 class ParseError(Exception):
37 37 """Exception raised on errors in parsing the command line."""
38 38
39 39 def showhelp():
40 40 sys.stdout.write(usage)
41 41 sys.stdout.write('\noptions:\n')
42 42
43 43 out_opts = []
44 44 for shortopt, longopt, default, desc in options:
45 45 out_opts.append(('%2s%s' % (shortopt and '-%s' % shortopt,
46 46 longopt and ' --%s' % longopt),
47 47 '%s' % desc))
48 48 opts_len = max([len(opt[0]) for opt in out_opts])
49 49 for first, second in out_opts:
50 50 sys.stdout.write(' %-*s %s\n' % (opts_len, first, second))
51 51
52 52 class filebackedctx(object):
53 53 """simplemerge requires context-like objects"""
54 54 def __init__(self, path):
55 55 self._path = path
56 56
57 57 def decodeddata(self):
58 58 with open(self._path, "rb") as f:
59 59 return f.read()
60 60
61 61 def flags(self):
62 62 return ''
63 63
64 64 def path(self):
65 65 return self._path
66 66
67 67 def write(self, data, flags):
68 68 assert not flags
69 69 with open(self._path, "w") as f:
70 70 f.write(data)
71 71
72 72 try:
73 73 for fp in (sys.stdin, sys.stdout, sys.stderr):
74 74 util.setbinary(fp)
75 75
76 76 opts = {}
77 77 try:
78 78 args = fancyopts.fancyopts(sys.argv[1:], options, opts)
79 79 except getopt.GetoptError as e:
80 80 raise ParseError(e)
81 81 if opts['help']:
82 82 showhelp()
83 83 sys.exit(0)
84 84 if len(args) != 3:
85 85 raise ParseError(_('wrong number of arguments'))
86 86 local, base, other = args
87 87 sys.exit(simplemerge.simplemerge(uimod.ui.load(),
88 local,
89 base,
90 other,
91 88 filebackedctx(local),
92 89 filebackedctx(base),
93 90 filebackedctx(other),
94 91 filtereddata=True,
95 92 **opts))
96 93 except ParseError as e:
97 94 sys.stdout.write("%s: %s\n" % (sys.argv[0], e))
98 95 showhelp()
99 96 sys.exit(1)
100 97 except error.Abort as e:
101 98 sys.stderr.write("abort: %s\n" % e)
102 99 sys.exit(255)
103 100 except KeyboardInterrupt:
104 101 sys.exit(255)
@@ -1,750 +1,750 b''
1 1 # filemerge.py - file-level merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import filecmp
11 11 import os
12 12 import re
13 13 import tempfile
14 14
15 15 from .i18n import _
16 16 from .node import nullid, short
17 17
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 formatter,
22 22 match,
23 23 pycompat,
24 24 registrar,
25 25 scmutil,
26 26 simplemerge,
27 27 tagmerge,
28 28 templatekw,
29 29 templater,
30 30 util,
31 31 )
32 32
33 33 def _toolstr(ui, tool, part, default=""):
34 34 return ui.config("merge-tools", tool + "." + part, default)
35 35
36 36 def _toolbool(ui, tool, part, default=False):
37 37 return ui.configbool("merge-tools", tool + "." + part, default)
38 38
39 39 def _toollist(ui, tool, part, default=None):
40 40 if default is None:
41 41 default = []
42 42 return ui.configlist("merge-tools", tool + "." + part, default)
43 43
44 44 internals = {}
45 45 # Merge tools to document.
46 46 internalsdoc = {}
47 47
48 48 internaltool = registrar.internalmerge()
49 49
50 50 # internal tool merge types
51 51 nomerge = internaltool.nomerge
52 52 mergeonly = internaltool.mergeonly # just the full merge, no premerge
53 53 fullmerge = internaltool.fullmerge # both premerge and merge
54 54
55 55 _localchangedotherdeletedmsg = _(
56 56 "local%(l)s changed %(fd)s which other%(o)s deleted\n"
57 57 "use (c)hanged version, (d)elete, or leave (u)nresolved?"
58 58 "$$ &Changed $$ &Delete $$ &Unresolved")
59 59
60 60 _otherchangedlocaldeletedmsg = _(
61 61 "other%(o)s changed %(fd)s which local%(l)s deleted\n"
62 62 "use (c)hanged version, leave (d)eleted, or "
63 63 "leave (u)nresolved?"
64 64 "$$ &Changed $$ &Deleted $$ &Unresolved")
65 65
66 66 class absentfilectx(object):
67 67 """Represents a file that's ostensibly in a context but is actually not
68 68 present in it.
69 69
70 70 This is here because it's very specific to the filemerge code for now --
71 71 other code is likely going to break with the values this returns."""
72 72 def __init__(self, ctx, f):
73 73 self._ctx = ctx
74 74 self._f = f
75 75
76 76 def path(self):
77 77 return self._f
78 78
79 79 def size(self):
80 80 return None
81 81
82 82 def data(self):
83 83 return None
84 84
85 85 def filenode(self):
86 86 return nullid
87 87
88 88 _customcmp = True
89 89 def cmp(self, fctx):
90 90 """compare with other file context
91 91
92 92 returns True if different from fctx.
93 93 """
94 94 return not (fctx.isabsent() and
95 95 fctx.ctx() == self.ctx() and
96 96 fctx.path() == self.path())
97 97
98 98 def flags(self):
99 99 return ''
100 100
101 101 def changectx(self):
102 102 return self._ctx
103 103
104 104 def isbinary(self):
105 105 return False
106 106
107 107 def isabsent(self):
108 108 return True
109 109
110 110 def _findtool(ui, tool):
111 111 if tool in internals:
112 112 return tool
113 113 return findexternaltool(ui, tool)
114 114
115 115 def findexternaltool(ui, tool):
116 116 for kn in ("regkey", "regkeyalt"):
117 117 k = _toolstr(ui, tool, kn)
118 118 if not k:
119 119 continue
120 120 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
121 121 if p:
122 122 p = util.findexe(p + _toolstr(ui, tool, "regappend"))
123 123 if p:
124 124 return p
125 125 exe = _toolstr(ui, tool, "executable", tool)
126 126 return util.findexe(util.expandpath(exe))
127 127
128 128 def _picktool(repo, ui, path, binary, symlink, changedelete):
129 129 def supportscd(tool):
130 130 return tool in internals and internals[tool].mergetype == nomerge
131 131
132 132 def check(tool, pat, symlink, binary, changedelete):
133 133 tmsg = tool
134 134 if pat:
135 135 tmsg = _("%s (for pattern %s)") % (tool, pat)
136 136 if not _findtool(ui, tool):
137 137 if pat: # explicitly requested tool deserves a warning
138 138 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
139 139 else: # configured but non-existing tools are more silent
140 140 ui.note(_("couldn't find merge tool %s\n") % tmsg)
141 141 elif symlink and not _toolbool(ui, tool, "symlink"):
142 142 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
143 143 elif binary and not _toolbool(ui, tool, "binary"):
144 144 ui.warn(_("tool %s can't handle binary\n") % tmsg)
145 145 elif changedelete and not supportscd(tool):
146 146 # the nomerge tools are the only tools that support change/delete
147 147 # conflicts
148 148 pass
149 149 elif not util.gui() and _toolbool(ui, tool, "gui"):
150 150 ui.warn(_("tool %s requires a GUI\n") % tmsg)
151 151 else:
152 152 return True
153 153 return False
154 154
155 155 # internal config: ui.forcemerge
156 156 # forcemerge comes from command line arguments, highest priority
157 157 force = ui.config('ui', 'forcemerge')
158 158 if force:
159 159 toolpath = _findtool(ui, force)
160 160 if changedelete and not supportscd(toolpath):
161 161 return ":prompt", None
162 162 else:
163 163 if toolpath:
164 164 return (force, util.shellquote(toolpath))
165 165 else:
166 166 # mimic HGMERGE if given tool not found
167 167 return (force, force)
168 168
169 169 # HGMERGE takes next precedence
170 170 hgmerge = encoding.environ.get("HGMERGE")
171 171 if hgmerge:
172 172 if changedelete and not supportscd(hgmerge):
173 173 return ":prompt", None
174 174 else:
175 175 return (hgmerge, hgmerge)
176 176
177 177 # then patterns
178 178 for pat, tool in ui.configitems("merge-patterns"):
179 179 mf = match.match(repo.root, '', [pat])
180 180 if mf(path) and check(tool, pat, symlink, False, changedelete):
181 181 toolpath = _findtool(ui, tool)
182 182 return (tool, util.shellquote(toolpath))
183 183
184 184 # then merge tools
185 185 tools = {}
186 186 disabled = set()
187 187 for k, v in ui.configitems("merge-tools"):
188 188 t = k.split('.')[0]
189 189 if t not in tools:
190 190 tools[t] = int(_toolstr(ui, t, "priority", "0"))
191 191 if _toolbool(ui, t, "disabled", False):
192 192 disabled.add(t)
193 193 names = tools.keys()
194 194 tools = sorted([(-p, tool) for tool, p in tools.items()
195 195 if tool not in disabled])
196 196 uimerge = ui.config("ui", "merge")
197 197 if uimerge:
198 198 # external tools defined in uimerge won't be able to handle
199 199 # change/delete conflicts
200 200 if uimerge not in names and not changedelete:
201 201 return (uimerge, uimerge)
202 202 tools.insert(0, (None, uimerge)) # highest priority
203 203 tools.append((None, "hgmerge")) # the old default, if found
204 204 for p, t in tools:
205 205 if check(t, None, symlink, binary, changedelete):
206 206 toolpath = _findtool(ui, t)
207 207 return (t, util.shellquote(toolpath))
208 208
209 209 # internal merge or prompt as last resort
210 210 if symlink or binary or changedelete:
211 211 if not changedelete and len(tools):
212 212 # any tool is rejected by capability for symlink or binary
213 213 ui.warn(_("no tool found to merge %s\n") % path)
214 214 return ":prompt", None
215 215 return ":merge", None
216 216
217 217 def _eoltype(data):
218 218 "Guess the EOL type of a file"
219 219 if '\0' in data: # binary
220 220 return None
221 221 if '\r\n' in data: # Windows
222 222 return '\r\n'
223 223 if '\r' in data: # Old Mac
224 224 return '\r'
225 225 if '\n' in data: # UNIX
226 226 return '\n'
227 227 return None # unknown
228 228
229 229 def _matcheol(file, origfile):
230 230 "Convert EOL markers in a file to match origfile"
231 231 tostyle = _eoltype(util.readfile(origfile))
232 232 if tostyle:
233 233 data = util.readfile(file)
234 234 style = _eoltype(data)
235 235 if style:
236 236 newdata = data.replace(style, tostyle)
237 237 if newdata != data:
238 238 util.writefile(file, newdata)
239 239
240 240 @internaltool('prompt', nomerge)
241 241 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
242 242 """Asks the user which of the local `p1()` or the other `p2()` version to
243 243 keep as the merged version."""
244 244 ui = repo.ui
245 245 fd = fcd.path()
246 246
247 247 prompts = partextras(labels)
248 248 prompts['fd'] = fd
249 249 try:
250 250 if fco.isabsent():
251 251 index = ui.promptchoice(
252 252 _localchangedotherdeletedmsg % prompts, 2)
253 253 choice = ['local', 'other', 'unresolved'][index]
254 254 elif fcd.isabsent():
255 255 index = ui.promptchoice(
256 256 _otherchangedlocaldeletedmsg % prompts, 2)
257 257 choice = ['other', 'local', 'unresolved'][index]
258 258 else:
259 259 index = ui.promptchoice(
260 260 _("keep (l)ocal%(l)s, take (o)ther%(o)s, or leave (u)nresolved"
261 261 " for %(fd)s?"
262 262 "$$ &Local $$ &Other $$ &Unresolved") % prompts, 2)
263 263 choice = ['local', 'other', 'unresolved'][index]
264 264
265 265 if choice == 'other':
266 266 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf,
267 267 labels)
268 268 elif choice == 'local':
269 269 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf,
270 270 labels)
271 271 elif choice == 'unresolved':
272 272 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
273 273 labels)
274 274 except error.ResponseExpected:
275 275 ui.write("\n")
276 276 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
277 277 labels)
278 278
279 279 @internaltool('local', nomerge)
280 280 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
281 281 """Uses the local `p1()` version of files as the merged version."""
282 282 return 0, fcd.isabsent()
283 283
284 284 @internaltool('other', nomerge)
285 285 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
286 286 """Uses the other `p2()` version of files as the merged version."""
287 287 if fco.isabsent():
288 288 # local changed, remote deleted -- 'deleted' picked
289 289 _underlyingfctxifabsent(fcd).remove()
290 290 deleted = True
291 291 else:
292 292 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
293 293 deleted = False
294 294 return 0, deleted
295 295
296 296 @internaltool('fail', nomerge)
297 297 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
298 298 """
299 299 Rather than attempting to merge files that were modified on both
300 300 branches, it marks them as unresolved. The resolve command must be
301 301 used to resolve these conflicts."""
302 302 # for change/delete conflicts write out the changed version, then fail
303 303 if fcd.isabsent():
304 304 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
305 305 return 1, False
306 306
307 307 def _underlyingfctxifabsent(filectx):
308 308 """Sometimes when resolving, our fcd is actually an absentfilectx, but
309 309 we want to write to it (to do the resolve). This helper returns the
310 310 underyling workingfilectx in that case.
311 311 """
312 312 if filectx.isabsent():
313 313 return filectx.changectx()[filectx.path()]
314 314 else:
315 315 return filectx
316 316
317 317 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
318 318 tool, toolpath, binary, symlink = toolconf
319 319 if symlink or fcd.isabsent() or fco.isabsent():
320 320 return 1
321 321 a, b, c, back = files
322 322
323 323 ui = repo.ui
324 324
325 325 validkeep = ['keep', 'keep-merge3']
326 326
327 327 # do we attempt to simplemerge first?
328 328 try:
329 329 premerge = _toolbool(ui, tool, "premerge", not binary)
330 330 except error.ConfigError:
331 331 premerge = _toolstr(ui, tool, "premerge").lower()
332 332 if premerge not in validkeep:
333 333 _valid = ', '.join(["'" + v + "'" for v in validkeep])
334 334 raise error.ConfigError(_("%s.premerge not valid "
335 335 "('%s' is neither boolean nor %s)") %
336 336 (tool, premerge, _valid))
337 337
338 338 if premerge:
339 339 if premerge == 'keep-merge3':
340 340 if not labels:
341 341 labels = _defaultconflictlabels
342 342 if len(labels) < 3:
343 343 labels.append('base')
344 r = simplemerge.simplemerge(ui, a, b, c, fcd, fca, fco,
344 r = simplemerge.simplemerge(ui, fcd, fca, fco,
345 345 quiet=True, label=labels, repo=repo)
346 346 if not r:
347 347 ui.debug(" premerge successful\n")
348 348 return 0
349 349 if premerge not in validkeep:
350 350 util.copyfile(back, a) # restore from backup and try again
351 351 return 1 # continue merging
352 352
353 353 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
354 354 tool, toolpath, binary, symlink = toolconf
355 355 if symlink:
356 356 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
357 357 'for %s\n') % (tool, fcd.path()))
358 358 return False
359 359 if fcd.isabsent() or fco.isabsent():
360 360 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
361 361 'conflict for %s\n') % (tool, fcd.path()))
362 362 return False
363 363 return True
364 364
365 365 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
366 366 """
367 367 Uses the internal non-interactive simple merge algorithm for merging
368 368 files. It will fail if there are any conflicts and leave markers in
369 369 the partially merged file. Markers will have two sections, one for each side
370 370 of merge, unless mode equals 'union' which suppresses the markers."""
371 371 a, b, c, back = files
372 372
373 373 ui = repo.ui
374 374
375 r = simplemerge.simplemerge(ui, a, b, c, fcd, fca, fco,
375 r = simplemerge.simplemerge(ui, fcd, fca, fco,
376 376 label=labels, mode=mode, repo=repo)
377 377 return True, r, False
378 378
379 379 @internaltool('union', fullmerge,
380 380 _("warning: conflicts while merging %s! "
381 381 "(edit, then use 'hg resolve --mark')\n"),
382 382 precheck=_mergecheck)
383 383 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
384 384 """
385 385 Uses the internal non-interactive simple merge algorithm for merging
386 386 files. It will use both left and right sides for conflict regions.
387 387 No markers are inserted."""
388 388 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
389 389 files, labels, 'union')
390 390
391 391 @internaltool('merge', fullmerge,
392 392 _("warning: conflicts while merging %s! "
393 393 "(edit, then use 'hg resolve --mark')\n"),
394 394 precheck=_mergecheck)
395 395 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
396 396 """
397 397 Uses the internal non-interactive simple merge algorithm for merging
398 398 files. It will fail if there are any conflicts and leave markers in
399 399 the partially merged file. Markers will have two sections, one for each side
400 400 of merge."""
401 401 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
402 402 files, labels, 'merge')
403 403
404 404 @internaltool('merge3', fullmerge,
405 405 _("warning: conflicts while merging %s! "
406 406 "(edit, then use 'hg resolve --mark')\n"),
407 407 precheck=_mergecheck)
408 408 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
409 409 """
410 410 Uses the internal non-interactive simple merge algorithm for merging
411 411 files. It will fail if there are any conflicts and leave markers in
412 412 the partially merged file. Marker will have three sections, one from each
413 413 side of the merge and one for the base content."""
414 414 if not labels:
415 415 labels = _defaultconflictlabels
416 416 if len(labels) < 3:
417 417 labels.append('base')
418 418 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
419 419
420 420 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
421 421 labels=None, localorother=None):
422 422 """
423 423 Generic driver for _imergelocal and _imergeother
424 424 """
425 425 assert localorother is not None
426 426 tool, toolpath, binary, symlink = toolconf
427 427 a, b, c, back = files
428 r = simplemerge.simplemerge(repo.ui, a, b, c, fcd, fca, fco,
428 r = simplemerge.simplemerge(repo.ui, fcd, fca, fco,
429 429 label=labels, localorother=localorother,
430 430 repo=repo)
431 431 return True, r
432 432
433 433 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
434 434 def _imergelocal(*args, **kwargs):
435 435 """
436 436 Like :merge, but resolve all conflicts non-interactively in favor
437 437 of the local `p1()` changes."""
438 438 success, status = _imergeauto(localorother='local', *args, **kwargs)
439 439 return success, status, False
440 440
441 441 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
442 442 def _imergeother(*args, **kwargs):
443 443 """
444 444 Like :merge, but resolve all conflicts non-interactively in favor
445 445 of the other `p2()` changes."""
446 446 success, status = _imergeauto(localorother='other', *args, **kwargs)
447 447 return success, status, False
448 448
449 449 @internaltool('tagmerge', mergeonly,
450 450 _("automatic tag merging of %s failed! "
451 451 "(use 'hg resolve --tool :merge' or another merge "
452 452 "tool of your choice)\n"))
453 453 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
454 454 """
455 455 Uses the internal tag merge algorithm (experimental).
456 456 """
457 457 success, status = tagmerge.merge(repo, fcd, fco, fca)
458 458 return success, status, False
459 459
460 460 @internaltool('dump', fullmerge)
461 461 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
462 462 """
463 463 Creates three versions of the files to merge, containing the
464 464 contents of local, other and base. These files can then be used to
465 465 perform a merge manually. If the file to be merged is named
466 466 ``a.txt``, these files will accordingly be named ``a.txt.local``,
467 467 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
468 468 same directory as ``a.txt``.
469 469
470 470 This implies permerge. Therefore, files aren't dumped, if premerge
471 471 runs successfully. Use :forcedump to forcibly write files out.
472 472 """
473 473 a, b, c, back = files
474 474
475 475 fd = fcd.path()
476 476
477 477 util.copyfile(a, a + ".local")
478 478 repo.wwrite(fd + ".other", fco.data(), fco.flags())
479 479 repo.wwrite(fd + ".base", fca.data(), fca.flags())
480 480 return False, 1, False
481 481
482 482 @internaltool('forcedump', mergeonly)
483 483 def _forcedump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
484 484 labels=None):
485 485 """
486 486 Creates three versions of the files as same as :dump, but omits premerge.
487 487 """
488 488 return _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
489 489 labels=labels)
490 490
491 491 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
492 492 tool, toolpath, binary, symlink = toolconf
493 493 if fcd.isabsent() or fco.isabsent():
494 494 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
495 495 'for %s\n') % (tool, fcd.path()))
496 496 return False, 1, None
497 497 a, b, c, back = files
498 498 out = ""
499 499 env = {'HG_FILE': fcd.path(),
500 500 'HG_MY_NODE': short(mynode),
501 501 'HG_OTHER_NODE': str(fco.changectx()),
502 502 'HG_BASE_NODE': str(fca.changectx()),
503 503 'HG_MY_ISLINK': 'l' in fcd.flags(),
504 504 'HG_OTHER_ISLINK': 'l' in fco.flags(),
505 505 'HG_BASE_ISLINK': 'l' in fca.flags(),
506 506 }
507 507
508 508 ui = repo.ui
509 509
510 510 args = _toolstr(ui, tool, "args", '$local $base $other')
511 511 if "$output" in args:
512 512 out, a = a, back # read input from backup, write to original
513 513 replace = {'local': a, 'base': b, 'other': c, 'output': out}
514 514 args = util.interpolate(r'\$', replace, args,
515 515 lambda s: util.shellquote(util.localpath(s)))
516 516 cmd = toolpath + ' ' + args
517 517 if _toolbool(ui, tool, "gui"):
518 518 repo.ui.status(_('running merge tool %s for file %s\n') %
519 519 (tool, fcd.path()))
520 520 repo.ui.debug('launching merge tool: %s\n' % cmd)
521 521 r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool')
522 522 repo.ui.debug('merge tool returned: %s\n' % r)
523 523 return True, r, False
524 524
525 525 def _formatconflictmarker(repo, ctx, template, label, pad):
526 526 """Applies the given template to the ctx, prefixed by the label.
527 527
528 528 Pad is the minimum width of the label prefix, so that multiple markers
529 529 can have aligned templated parts.
530 530 """
531 531 if ctx.node() is None:
532 532 ctx = ctx.p1()
533 533
534 534 props = templatekw.keywords.copy()
535 535 props['templ'] = template
536 536 props['ctx'] = ctx
537 537 props['repo'] = repo
538 538 templateresult = template.render(props)
539 539
540 540 label = ('%s:' % label).ljust(pad + 1)
541 541 mark = '%s %s' % (label, templateresult)
542 542
543 543 if mark:
544 544 mark = mark.splitlines()[0] # split for safety
545 545
546 546 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
547 547 return util.ellipsis(mark, 80 - 8)
548 548
549 549 _defaultconflictlabels = ['local', 'other']
550 550
551 551 def _formatlabels(repo, fcd, fco, fca, labels):
552 552 """Formats the given labels using the conflict marker template.
553 553
554 554 Returns a list of formatted labels.
555 555 """
556 556 cd = fcd.changectx()
557 557 co = fco.changectx()
558 558 ca = fca.changectx()
559 559
560 560 ui = repo.ui
561 561 template = ui.config('ui', 'mergemarkertemplate')
562 562 template = templater.unquotestring(template)
563 563 tmpl = formatter.maketemplater(ui, template)
564 564
565 565 pad = max(len(l) for l in labels)
566 566
567 567 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
568 568 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
569 569 if len(labels) > 2:
570 570 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
571 571 return newlabels
572 572
573 573 def partextras(labels):
574 574 """Return a dictionary of extra labels for use in prompts to the user
575 575
576 576 Intended use is in strings of the form "(l)ocal%(l)s".
577 577 """
578 578 if labels is None:
579 579 return {
580 580 "l": "",
581 581 "o": "",
582 582 }
583 583
584 584 return {
585 585 "l": " [%s]" % labels[0],
586 586 "o": " [%s]" % labels[1],
587 587 }
588 588
589 589 def _filemerge(premerge, repo, mynode, orig, fcd, fco, fca, labels=None):
590 590 """perform a 3-way merge in the working directory
591 591
592 592 premerge = whether this is a premerge
593 593 mynode = parent node before merge
594 594 orig = original local filename before merge
595 595 fco = other file context
596 596 fca = ancestor file context
597 597 fcd = local file context for current/destination file
598 598
599 599 Returns whether the merge is complete, the return value of the merge, and
600 600 a boolean indicating whether the file was deleted from disk."""
601 601
602 602 def temp(prefix, ctx):
603 603 fullbase, ext = os.path.splitext(ctx.path())
604 604 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
605 605 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
606 606 data = repo.wwritedata(ctx.path(), ctx.data())
607 607 f = os.fdopen(fd, pycompat.sysstr("wb"))
608 608 f.write(data)
609 609 f.close()
610 610 return name
611 611
612 612 if not fco.cmp(fcd): # files identical?
613 613 return True, None, False
614 614
615 615 ui = repo.ui
616 616 fd = fcd.path()
617 617 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
618 618 symlink = 'l' in fcd.flags() + fco.flags()
619 619 changedelete = fcd.isabsent() or fco.isabsent()
620 620 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
621 621 if tool in internals and tool.startswith('internal:'):
622 622 # normalize to new-style names (':merge' etc)
623 623 tool = tool[len('internal'):]
624 624 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
625 625 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
626 626 pycompat.bytestr(changedelete)))
627 627
628 628 if tool in internals:
629 629 func = internals[tool]
630 630 mergetype = func.mergetype
631 631 onfailure = func.onfailure
632 632 precheck = func.precheck
633 633 else:
634 634 func = _xmerge
635 635 mergetype = fullmerge
636 636 onfailure = _("merging %s failed!\n")
637 637 precheck = None
638 638
639 639 toolconf = tool, toolpath, binary, symlink
640 640
641 641 if mergetype == nomerge:
642 642 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
643 643 return True, r, deleted
644 644
645 645 if premerge:
646 646 if orig != fco.path():
647 647 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
648 648 else:
649 649 ui.status(_("merging %s\n") % fd)
650 650
651 651 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
652 652
653 653 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
654 654 toolconf):
655 655 if onfailure:
656 656 ui.warn(onfailure % fd)
657 657 return True, 1, False
658 658
659 659 a = repo.wjoin(fd)
660 660 b = temp("base", fca)
661 661 c = temp("other", fco)
662 662 if not fcd.isabsent():
663 663 back = scmutil.origpath(ui, repo, a)
664 664 if premerge:
665 665 util.copyfile(a, back)
666 666 else:
667 667 back = None
668 668 files = (a, b, c, back)
669 669
670 670 r = 1
671 671 try:
672 672 markerstyle = ui.config('ui', 'mergemarkers')
673 673 if not labels:
674 674 labels = _defaultconflictlabels
675 675 if markerstyle != 'basic':
676 676 labels = _formatlabels(repo, fcd, fco, fca, labels)
677 677
678 678 if premerge and mergetype == fullmerge:
679 679 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
680 680 # complete if premerge successful (r is 0)
681 681 return not r, r, False
682 682
683 683 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
684 684 toolconf, files, labels=labels)
685 685
686 686 if needcheck:
687 687 r = _check(r, ui, tool, fcd, files)
688 688
689 689 if r:
690 690 if onfailure:
691 691 ui.warn(onfailure % fd)
692 692
693 693 return True, r, deleted
694 694 finally:
695 695 if not r and back is not None:
696 696 util.unlink(back)
697 697 util.unlink(b)
698 698 util.unlink(c)
699 699
700 700 def _check(r, ui, tool, fcd, files):
701 701 fd = fcd.path()
702 702 a, b, c, back = files
703 703
704 704 if not r and (_toolbool(ui, tool, "checkconflicts") or
705 705 'conflicts' in _toollist(ui, tool, "check")):
706 706 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
707 707 re.MULTILINE):
708 708 r = 1
709 709
710 710 checked = False
711 711 if 'prompt' in _toollist(ui, tool, "check"):
712 712 checked = True
713 713 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
714 714 "$$ &Yes $$ &No") % fd, 1):
715 715 r = 1
716 716
717 717 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
718 718 'changed' in
719 719 _toollist(ui, tool, "check")):
720 720 if back is not None and filecmp.cmp(a, back):
721 721 if ui.promptchoice(_(" output file %s appears unchanged\n"
722 722 "was merge successful (yn)?"
723 723 "$$ &Yes $$ &No") % fd, 1):
724 724 r = 1
725 725
726 726 if back is not None and _toolbool(ui, tool, "fixeol"):
727 727 _matcheol(a, back)
728 728
729 729 return r
730 730
731 731 def premerge(repo, mynode, orig, fcd, fco, fca, labels=None):
732 732 return _filemerge(True, repo, mynode, orig, fcd, fco, fca, labels=labels)
733 733
734 734 def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
735 735 return _filemerge(False, repo, mynode, orig, fcd, fco, fca, labels=labels)
736 736
737 737 def loadinternalmerge(ui, extname, registrarobj):
738 738 """Load internal merge tool from specified registrarobj
739 739 """
740 740 for name, func in registrarobj._table.iteritems():
741 741 fullname = ':' + name
742 742 internals[fullname] = func
743 743 internals['internal:' + name] = func
744 744 internalsdoc[fullname] = func
745 745
746 746 # load built-in merge tools explicitly to setup internalsdoc
747 747 loadinternalmerge(None, None, internaltool)
748 748
749 749 # tell hggettext to extract docstrings from these functions:
750 750 i18nfunctions = internals.values()
@@ -1,493 +1,493 b''
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 util,
27 27 )
28 28
29 29 class CantReprocessAndShowBase(Exception):
30 30 pass
31 31
32 32 def intersect(ra, rb):
33 33 """Given two ranges return the range where they intersect or None.
34 34
35 35 >>> intersect((0, 10), (0, 6))
36 36 (0, 6)
37 37 >>> intersect((0, 10), (5, 15))
38 38 (5, 10)
39 39 >>> intersect((0, 10), (10, 15))
40 40 >>> intersect((0, 9), (10, 15))
41 41 >>> intersect((0, 9), (7, 15))
42 42 (7, 9)
43 43 """
44 44 assert ra[0] <= ra[1]
45 45 assert rb[0] <= rb[1]
46 46
47 47 sa = max(ra[0], rb[0])
48 48 sb = min(ra[1], rb[1])
49 49 if sa < sb:
50 50 return sa, sb
51 51 else:
52 52 return None
53 53
54 54 def compare_range(a, astart, aend, b, bstart, bend):
55 55 """Compare a[astart:aend] == b[bstart:bend], without slicing.
56 56 """
57 57 if (aend - astart) != (bend - bstart):
58 58 return False
59 59 for ia, ib in zip(xrange(astart, aend), xrange(bstart, bend)):
60 60 if a[ia] != b[ib]:
61 61 return False
62 62 else:
63 63 return True
64 64
65 65 class Merge3Text(object):
66 66 """3-way merge of texts.
67 67
68 68 Given strings BASE, OTHER, THIS, tries to produce a combined text
69 69 incorporating the changes from both BASE->OTHER and BASE->THIS."""
70 70 def __init__(self, basetext, atext, btext, base=None, a=None, b=None):
71 71 self.basetext = basetext
72 72 self.atext = atext
73 73 self.btext = btext
74 74 if base is None:
75 75 base = mdiff.splitnewlines(basetext)
76 76 if a is None:
77 77 a = mdiff.splitnewlines(atext)
78 78 if b is None:
79 79 b = mdiff.splitnewlines(btext)
80 80 self.base = base
81 81 self.a = a
82 82 self.b = b
83 83
84 84 def merge_lines(self,
85 85 name_a=None,
86 86 name_b=None,
87 87 name_base=None,
88 88 start_marker='<<<<<<<',
89 89 mid_marker='=======',
90 90 end_marker='>>>>>>>',
91 91 base_marker=None,
92 92 localorother=None,
93 93 minimize=False):
94 94 """Return merge in cvs-like form.
95 95 """
96 96 self.conflicts = False
97 97 newline = '\n'
98 98 if len(self.a) > 0:
99 99 if self.a[0].endswith('\r\n'):
100 100 newline = '\r\n'
101 101 elif self.a[0].endswith('\r'):
102 102 newline = '\r'
103 103 if name_a and start_marker:
104 104 start_marker = start_marker + ' ' + name_a
105 105 if name_b and end_marker:
106 106 end_marker = end_marker + ' ' + name_b
107 107 if name_base and base_marker:
108 108 base_marker = base_marker + ' ' + name_base
109 109 merge_regions = self.merge_regions()
110 110 if minimize:
111 111 merge_regions = self.minimize(merge_regions)
112 112 for t in merge_regions:
113 113 what = t[0]
114 114 if what == 'unchanged':
115 115 for i in range(t[1], t[2]):
116 116 yield self.base[i]
117 117 elif what == 'a' or what == 'same':
118 118 for i in range(t[1], t[2]):
119 119 yield self.a[i]
120 120 elif what == 'b':
121 121 for i in range(t[1], t[2]):
122 122 yield self.b[i]
123 123 elif what == 'conflict':
124 124 if localorother == 'local':
125 125 for i in range(t[3], t[4]):
126 126 yield self.a[i]
127 127 elif localorother == 'other':
128 128 for i in range(t[5], t[6]):
129 129 yield self.b[i]
130 130 else:
131 131 self.conflicts = True
132 132 if start_marker is not None:
133 133 yield start_marker + newline
134 134 for i in range(t[3], t[4]):
135 135 yield self.a[i]
136 136 if base_marker is not None:
137 137 yield base_marker + newline
138 138 for i in range(t[1], t[2]):
139 139 yield self.base[i]
140 140 if mid_marker is not None:
141 141 yield mid_marker + newline
142 142 for i in range(t[5], t[6]):
143 143 yield self.b[i]
144 144 if end_marker is not None:
145 145 yield end_marker + newline
146 146 else:
147 147 raise ValueError(what)
148 148
149 149 def merge_groups(self):
150 150 """Yield sequence of line groups. Each one is a tuple:
151 151
152 152 'unchanged', lines
153 153 Lines unchanged from base
154 154
155 155 'a', lines
156 156 Lines taken from a
157 157
158 158 'same', lines
159 159 Lines taken from a (and equal to b)
160 160
161 161 'b', lines
162 162 Lines taken from b
163 163
164 164 'conflict', base_lines, a_lines, b_lines
165 165 Lines from base were changed to either a or b and conflict.
166 166 """
167 167 for t in self.merge_regions():
168 168 what = t[0]
169 169 if what == 'unchanged':
170 170 yield what, self.base[t[1]:t[2]]
171 171 elif what == 'a' or what == 'same':
172 172 yield what, self.a[t[1]:t[2]]
173 173 elif what == 'b':
174 174 yield what, self.b[t[1]:t[2]]
175 175 elif what == 'conflict':
176 176 yield (what,
177 177 self.base[t[1]:t[2]],
178 178 self.a[t[3]:t[4]],
179 179 self.b[t[5]:t[6]])
180 180 else:
181 181 raise ValueError(what)
182 182
183 183 def merge_regions(self):
184 184 """Return sequences of matching and conflicting regions.
185 185
186 186 This returns tuples, where the first value says what kind we
187 187 have:
188 188
189 189 'unchanged', start, end
190 190 Take a region of base[start:end]
191 191
192 192 'same', astart, aend
193 193 b and a are different from base but give the same result
194 194
195 195 'a', start, end
196 196 Non-clashing insertion from a[start:end]
197 197
198 198 'conflict', zstart, zend, astart, aend, bstart, bend
199 199 Conflict between a and b, with z as common ancestor
200 200
201 201 Method is as follows:
202 202
203 203 The two sequences align only on regions which match the base
204 204 and both descendants. These are found by doing a two-way diff
205 205 of each one against the base, and then finding the
206 206 intersections between those regions. These "sync regions"
207 207 are by definition unchanged in both and easily dealt with.
208 208
209 209 The regions in between can be in any of three cases:
210 210 conflicted, or changed on only one side.
211 211 """
212 212
213 213 # section a[0:ia] has been disposed of, etc
214 214 iz = ia = ib = 0
215 215
216 216 for region in self.find_sync_regions():
217 217 zmatch, zend, amatch, aend, bmatch, bend = region
218 218 #print 'match base [%d:%d]' % (zmatch, zend)
219 219
220 220 matchlen = zend - zmatch
221 221 assert matchlen >= 0
222 222 assert matchlen == (aend - amatch)
223 223 assert matchlen == (bend - bmatch)
224 224
225 225 len_a = amatch - ia
226 226 len_b = bmatch - ib
227 227 len_base = zmatch - iz
228 228 assert len_a >= 0
229 229 assert len_b >= 0
230 230 assert len_base >= 0
231 231
232 232 #print 'unmatched a=%d, b=%d' % (len_a, len_b)
233 233
234 234 if len_a or len_b:
235 235 # try to avoid actually slicing the lists
236 236 equal_a = compare_range(self.a, ia, amatch,
237 237 self.base, iz, zmatch)
238 238 equal_b = compare_range(self.b, ib, bmatch,
239 239 self.base, iz, zmatch)
240 240 same = compare_range(self.a, ia, amatch,
241 241 self.b, ib, bmatch)
242 242
243 243 if same:
244 244 yield 'same', ia, amatch
245 245 elif equal_a and not equal_b:
246 246 yield 'b', ib, bmatch
247 247 elif equal_b and not equal_a:
248 248 yield 'a', ia, amatch
249 249 elif not equal_a and not equal_b:
250 250 yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch
251 251 else:
252 252 raise AssertionError("can't handle a=b=base but unmatched")
253 253
254 254 ia = amatch
255 255 ib = bmatch
256 256 iz = zmatch
257 257
258 258 # if the same part of the base was deleted on both sides
259 259 # that's OK, we can just skip it.
260 260
261 261
262 262 if matchlen > 0:
263 263 assert ia == amatch
264 264 assert ib == bmatch
265 265 assert iz == zmatch
266 266
267 267 yield 'unchanged', zmatch, zend
268 268 iz = zend
269 269 ia = aend
270 270 ib = bend
271 271
272 272 def minimize(self, merge_regions):
273 273 """Trim conflict regions of lines where A and B sides match.
274 274
275 275 Lines where both A and B have made the same changes at the beginning
276 276 or the end of each merge region are eliminated from the conflict
277 277 region and are instead considered the same.
278 278 """
279 279 for region in merge_regions:
280 280 if region[0] != "conflict":
281 281 yield region
282 282 continue
283 283 issue, z1, z2, a1, a2, b1, b2 = region
284 284 alen = a2 - a1
285 285 blen = b2 - b1
286 286
287 287 # find matches at the front
288 288 ii = 0
289 289 while ii < alen and ii < blen and \
290 290 self.a[a1 + ii] == self.b[b1 + ii]:
291 291 ii += 1
292 292 startmatches = ii
293 293
294 294 # find matches at the end
295 295 ii = 0
296 296 while ii < alen and ii < blen and \
297 297 self.a[a2 - ii - 1] == self.b[b2 - ii - 1]:
298 298 ii += 1
299 299 endmatches = ii
300 300
301 301 if startmatches > 0:
302 302 yield 'same', a1, a1 + startmatches
303 303
304 304 yield ('conflict', z1, z2,
305 305 a1 + startmatches, a2 - endmatches,
306 306 b1 + startmatches, b2 - endmatches)
307 307
308 308 if endmatches > 0:
309 309 yield 'same', a2 - endmatches, a2
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], self.a[asub:aend])
352 352
353 353 assert self.base[intbase:intend] == self.b[bsub:bend]
354 354
355 355 sl.append((intbase, intend,
356 356 asub, aend,
357 357 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 def find_unconflicted(self):
373 373 """Return a list of ranges in base that are not conflicted."""
374 374 am = mdiff.get_matching_blocks(self.basetext, self.atext)
375 375 bm = mdiff.get_matching_blocks(self.basetext, self.btext)
376 376
377 377 unc = []
378 378
379 379 while am and bm:
380 380 # there is an unconflicted block at i; how long does it
381 381 # extend? until whichever one ends earlier.
382 382 a1 = am[0][0]
383 383 a2 = a1 + am[0][2]
384 384 b1 = bm[0][0]
385 385 b2 = b1 + bm[0][2]
386 386 i = intersect((a1, a2), (b1, b2))
387 387 if i:
388 388 unc.append(i)
389 389
390 390 if a2 < b2:
391 391 del am[0]
392 392 else:
393 393 del bm[0]
394 394
395 395 return unc
396 396
397 397 def _verifytext(text, path, ui, opts):
398 398 """verifies that text is non-binary (unless opts[text] is passed,
399 399 then we just warn)"""
400 400 if util.binary(text):
401 401 msg = _("%s looks like a binary file.") % path
402 402 if not opts.get('quiet'):
403 403 ui.warn(_('warning: %s\n') % msg)
404 404 if not opts.get('text'):
405 405 raise error.Abort(msg)
406 406 return text
407 407
408 408 def _picklabels(defaults, overrides):
409 409 name_a, name_b, name_base = defaults
410 410
411 411 if len(overrides) > 0:
412 412 name_a = overrides[0]
413 413 if len(overrides) > 1:
414 414 name_b = overrides[1]
415 415 if len(overrides) > 2:
416 416 name_base = overrides[2]
417 417 if len(overrides) > 3:
418 418 raise error.Abort(_("can only specify three labels."))
419 419
420 420 return [name_a, name_b, name_base]
421 421
422 def simplemerge(ui, localfile, basefile, otherfile,
423 localctx=None, basectx=None, otherctx=None, repo=None, **opts):
422 def simplemerge(ui, localctx=None, basectx=None, otherctx=None, repo=None,
423 **opts):
424 424 """Performs the simplemerge algorithm.
425 425
426 426 {local|base|other}ctx are optional. If passed, they (local/base/other) will
427 427 be read from and the merge result written to (local). You should pass
428 428 explicit labels in this mode since the default is to use the file paths.
429 429 """
430 430 def readctx(ctx):
431 431 if not ctx:
432 432 return None
433 433 # Merges were always run in the working copy before, which means
434 434 # they used decoded data, if the user defined any repository
435 435 # filters.
436 436 #
437 437 # Maintain that behavior today for BC, though perhaps in the future
438 438 # it'd be worth considering whether merging encoded data (what the
439 439 # repository usually sees) might be more useful.
440 440 return _verifytext(ctx.decodeddata(), ctx.path(), ui, opts)
441 441
442 442 class ctxwriter(object):
443 443 def __init__(self, ctx):
444 444 self.ctx = ctx
445 445 self.text = ""
446 446
447 447 def write(self, text):
448 448 self.text += text
449 449
450 450 def close(self):
451 451 self.ctx.write(self.text, self.ctx.flags())
452 452
453 453 mode = opts.get('mode','merge')
454 454 name_a, name_b, name_base = None, None, None
455 455 if mode != 'union':
456 456 name_a, name_b, name_base = _picklabels([localctx.path(),
457 457 otherctx.path(), None],
458 458 opts.get('label', []))
459 459
460 460 try:
461 461 localtext = readctx(localctx)
462 462 basetext = readctx(basectx)
463 463 othertext = readctx(otherctx)
464 464 except error.Abort:
465 465 return 1
466 466
467 467 if opts.get('print'):
468 468 out = ui.fout
469 469 else:
470 470 out = ctxwriter(localctx)
471 471
472 472 m3 = Merge3Text(basetext, localtext, othertext)
473 473 extrakwargs = {
474 474 "localorother": opts.get("localorother", None),
475 475 'minimize': True,
476 476 }
477 477 if mode == 'union':
478 478 extrakwargs['start_marker'] = None
479 479 extrakwargs['mid_marker'] = None
480 480 extrakwargs['end_marker'] = None
481 481 elif name_base is not None:
482 482 extrakwargs['base_marker'] = '|||||||'
483 483 extrakwargs['name_base'] = name_base
484 484 extrakwargs['minimize'] = False
485 485 for line in m3.merge_lines(name_a=name_a, name_b=name_b,
486 486 **pycompat.strkwargs(extrakwargs)):
487 487 out.write(line)
488 488
489 489 if not opts.get('print'):
490 490 out.close()
491 491
492 492 if m3.conflicts and not mode == 'union':
493 493 return 1
General Comments 0
You need to be logged in to leave comments. Login now