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