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