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