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