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