##// END OF EJS Templates
merge: define conflict marker labels in filemerge()...
Durham Goode -
r21273:20b8090d default
parent child Browse files
Show More
@@ -1,378 +1,378
1 1 # filemerge.py - file-level merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import short
9 9 from i18n import _
10 10 import util, simplemerge, match, error
11 11 import os, tempfile, re, filecmp
12 12
13 13 def _toolstr(ui, tool, part, default=""):
14 14 return ui.config("merge-tools", tool + "." + part, default)
15 15
16 16 def _toolbool(ui, tool, part, default=False):
17 17 return ui.configbool("merge-tools", tool + "." + part, default)
18 18
19 19 def _toollist(ui, tool, part, default=[]):
20 20 return ui.configlist("merge-tools", tool + "." + part, default)
21 21
22 22 internals = {}
23 23
24 24 def internaltool(name, trymerge, onfailure=None):
25 25 '''return a decorator for populating internal merge tool table'''
26 26 def decorator(func):
27 27 fullname = 'internal:' + name
28 28 func.__doc__ = "``%s``\n" % fullname + func.__doc__.strip()
29 29 internals[fullname] = func
30 30 func.trymerge = trymerge
31 31 func.onfailure = onfailure
32 32 return func
33 33 return decorator
34 34
35 35 def _findtool(ui, tool):
36 36 if tool in internals:
37 37 return tool
38 38 for kn in ("regkey", "regkeyalt"):
39 39 k = _toolstr(ui, tool, kn)
40 40 if not k:
41 41 continue
42 42 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
43 43 if p:
44 44 p = util.findexe(p + _toolstr(ui, tool, "regappend"))
45 45 if p:
46 46 return p
47 47 exe = _toolstr(ui, tool, "executable", tool)
48 48 return util.findexe(util.expandpath(exe))
49 49
50 50 def _picktool(repo, ui, path, binary, symlink):
51 51 def check(tool, pat, symlink, binary):
52 52 tmsg = tool
53 53 if pat:
54 54 tmsg += " specified for " + pat
55 55 if not _findtool(ui, tool):
56 56 if pat: # explicitly requested tool deserves a warning
57 57 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
58 58 else: # configured but non-existing tools are more silent
59 59 ui.note(_("couldn't find merge tool %s\n") % tmsg)
60 60 elif symlink and not _toolbool(ui, tool, "symlink"):
61 61 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
62 62 elif binary and not _toolbool(ui, tool, "binary"):
63 63 ui.warn(_("tool %s can't handle binary\n") % tmsg)
64 64 elif not util.gui() and _toolbool(ui, tool, "gui"):
65 65 ui.warn(_("tool %s requires a GUI\n") % tmsg)
66 66 else:
67 67 return True
68 68 return False
69 69
70 70 # forcemerge comes from command line arguments, highest priority
71 71 force = ui.config('ui', 'forcemerge')
72 72 if force:
73 73 toolpath = _findtool(ui, force)
74 74 if toolpath:
75 75 return (force, util.shellquote(toolpath))
76 76 else:
77 77 # mimic HGMERGE if given tool not found
78 78 return (force, force)
79 79
80 80 # HGMERGE takes next precedence
81 81 hgmerge = os.environ.get("HGMERGE")
82 82 if hgmerge:
83 83 return (hgmerge, hgmerge)
84 84
85 85 # then patterns
86 86 for pat, tool in ui.configitems("merge-patterns"):
87 87 mf = match.match(repo.root, '', [pat])
88 88 if mf(path) and check(tool, pat, symlink, False):
89 89 toolpath = _findtool(ui, tool)
90 90 return (tool, util.shellquote(toolpath))
91 91
92 92 # then merge tools
93 93 tools = {}
94 94 for k, v in ui.configitems("merge-tools"):
95 95 t = k.split('.')[0]
96 96 if t not in tools:
97 97 tools[t] = int(_toolstr(ui, t, "priority", "0"))
98 98 names = tools.keys()
99 99 tools = sorted([(-p, t) for t, p in tools.items()])
100 100 uimerge = ui.config("ui", "merge")
101 101 if uimerge:
102 102 if uimerge not in names:
103 103 return (uimerge, uimerge)
104 104 tools.insert(0, (None, uimerge)) # highest priority
105 105 tools.append((None, "hgmerge")) # the old default, if found
106 106 for p, t in tools:
107 107 if check(t, None, symlink, binary):
108 108 toolpath = _findtool(ui, t)
109 109 return (t, util.shellquote(toolpath))
110 110
111 111 # internal merge or prompt as last resort
112 112 if symlink or binary:
113 113 return "internal:prompt", None
114 114 return "internal:merge", None
115 115
116 116 def _eoltype(data):
117 117 "Guess the EOL type of a file"
118 118 if '\0' in data: # binary
119 119 return None
120 120 if '\r\n' in data: # Windows
121 121 return '\r\n'
122 122 if '\r' in data: # Old Mac
123 123 return '\r'
124 124 if '\n' in data: # UNIX
125 125 return '\n'
126 126 return None # unknown
127 127
128 128 def _matcheol(file, origfile):
129 129 "Convert EOL markers in a file to match origfile"
130 130 tostyle = _eoltype(util.readfile(origfile))
131 131 if tostyle:
132 132 data = util.readfile(file)
133 133 style = _eoltype(data)
134 134 if style:
135 135 newdata = data.replace(style, tostyle)
136 136 if newdata != data:
137 137 util.writefile(file, newdata)
138 138
139 139 @internaltool('prompt', False)
140 140 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf):
141 141 """Asks the user which of the local or the other version to keep as
142 142 the merged version."""
143 143 ui = repo.ui
144 144 fd = fcd.path()
145 145
146 146 if ui.promptchoice(_(" no tool found to merge %s\n"
147 147 "keep (l)ocal or take (o)ther?"
148 148 "$$ &Local $$ &Other") % fd, 0):
149 149 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf)
150 150 else:
151 151 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf)
152 152
153 153 @internaltool('local', False)
154 154 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf):
155 155 """Uses the local version of files as the merged version."""
156 156 return 0
157 157
158 158 @internaltool('other', False)
159 159 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf):
160 160 """Uses the other version of files as the merged version."""
161 161 repo.wwrite(fcd.path(), fco.data(), fco.flags())
162 162 return 0
163 163
164 164 @internaltool('fail', False)
165 165 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf):
166 166 """
167 167 Rather than attempting to merge files that were modified on both
168 168 branches, it marks them as unresolved. The resolve command must be
169 169 used to resolve these conflicts."""
170 170 return 1
171 171
172 def _premerge(repo, toolconf, files):
172 def _premerge(repo, toolconf, files, labels=None):
173 173 tool, toolpath, binary, symlink = toolconf
174 174 if symlink:
175 175 return 1
176 176 a, b, c, back = files
177 177
178 178 ui = repo.ui
179 179
180 180 # do we attempt to simplemerge first?
181 181 try:
182 182 premerge = _toolbool(ui, tool, "premerge", not binary)
183 183 except error.ConfigError:
184 184 premerge = _toolstr(ui, tool, "premerge").lower()
185 185 valid = 'keep'.split()
186 186 if premerge not in valid:
187 187 _valid = ', '.join(["'" + v + "'" for v in valid])
188 188 raise error.ConfigError(_("%s.premerge not valid "
189 189 "('%s' is neither boolean nor %s)") %
190 190 (tool, premerge, _valid))
191 191
192 192 if premerge:
193 r = simplemerge.simplemerge(ui, a, b, c, quiet=True)
193 r = simplemerge.simplemerge(ui, a, b, c, quiet=True, label=labels)
194 194 if not r:
195 195 ui.debug(" premerge successful\n")
196 196 return 0
197 197 if premerge != 'keep':
198 198 util.copyfile(back, a) # restore from backup and try again
199 199 return 1 # continue merging
200 200
201 201 @internaltool('merge', True,
202 202 _("merging %s incomplete! "
203 203 "(edit conflicts, then use 'hg resolve --mark')\n"))
204 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files):
204 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
205 205 """
206 206 Uses the internal non-interactive simple merge algorithm for merging
207 207 files. It will fail if there are any conflicts and leave markers in
208 208 the partially merged file."""
209 209 tool, toolpath, binary, symlink = toolconf
210 210 if symlink:
211 211 repo.ui.warn(_('warning: internal:merge cannot merge symlinks '
212 212 'for %s\n') % fcd.path())
213 213 return False, 1
214
215 r = _premerge(repo, toolconf, files)
214 r = _premerge(repo, toolconf, files, labels=labels)
216 215 if r:
217 216 a, b, c, back = files
218 217
219 218 ui = repo.ui
220 219
221 r = simplemerge.simplemerge(ui, a, b, c, label=['local', 'other'])
220 r = simplemerge.simplemerge(ui, a, b, c, label=labels)
222 221 return True, r
223 222 return False, 0
224 223
225 224 @internaltool('dump', True)
226 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files):
225 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
227 226 """
228 227 Creates three versions of the files to merge, containing the
229 228 contents of local, other and base. These files can then be used to
230 229 perform a merge manually. If the file to be merged is named
231 230 ``a.txt``, these files will accordingly be named ``a.txt.local``,
232 231 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
233 232 same directory as ``a.txt``."""
234 r = _premerge(repo, toolconf, files)
233 r = _premerge(repo, toolconf, files, labels=labels)
235 234 if r:
236 235 a, b, c, back = files
237 236
238 237 fd = fcd.path()
239 238
240 239 util.copyfile(a, a + ".local")
241 240 repo.wwrite(fd + ".other", fco.data(), fco.flags())
242 241 repo.wwrite(fd + ".base", fca.data(), fca.flags())
243 242 return False, r
244 243
245 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files):
246 r = _premerge(repo, toolconf, files)
244 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
245 r = _premerge(repo, toolconf, files, labels=labels)
247 246 if r:
248 247 tool, toolpath, binary, symlink = toolconf
249 248 a, b, c, back = files
250 249 out = ""
251 250 env = {'HG_FILE': fcd.path(),
252 251 'HG_MY_NODE': short(mynode),
253 252 'HG_OTHER_NODE': str(fco.changectx()),
254 253 'HG_BASE_NODE': str(fca.changectx()),
255 254 'HG_MY_ISLINK': 'l' in fcd.flags(),
256 255 'HG_OTHER_ISLINK': 'l' in fco.flags(),
257 256 'HG_BASE_ISLINK': 'l' in fca.flags(),
258 257 }
259 258
260 259 ui = repo.ui
261 260
262 261 args = _toolstr(ui, tool, "args", '$local $base $other')
263 262 if "$output" in args:
264 263 out, a = a, back # read input from backup, write to original
265 264 replace = {'local': a, 'base': b, 'other': c, 'output': out}
266 265 args = util.interpolate(r'\$', replace, args,
267 266 lambda s: util.shellquote(util.localpath(s)))
268 267 r = util.system(toolpath + ' ' + args, cwd=repo.root, environ=env,
269 268 out=ui.fout)
270 269 return True, r
271 270 return False, 0
272 271
273 272 def filemerge(repo, mynode, orig, fcd, fco, fca):
274 273 """perform a 3-way merge in the working directory
275 274
276 275 mynode = parent node before merge
277 276 orig = original local filename before merge
278 277 fco = other file context
279 278 fca = ancestor file context
280 279 fcd = local file context for current/destination file
281 280 """
282 281
283 282 def temp(prefix, ctx):
284 283 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
285 284 (fd, name) = tempfile.mkstemp(prefix=pre)
286 285 data = repo.wwritedata(ctx.path(), ctx.data())
287 286 f = os.fdopen(fd, "wb")
288 287 f.write(data)
289 288 f.close()
290 289 return name
291 290
292 291 if not fco.cmp(fcd): # files identical?
293 292 return None
294 293
295 294 ui = repo.ui
296 295 fd = fcd.path()
297 296 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
298 297 symlink = 'l' in fcd.flags() + fco.flags()
299 298 tool, toolpath = _picktool(repo, ui, fd, binary, symlink)
300 299 ui.debug("picked tool '%s' for %s (binary %s symlink %s)\n" %
301 300 (tool, fd, binary, symlink))
302 301
303 302 if tool in internals:
304 303 func = internals[tool]
305 304 trymerge = func.trymerge
306 305 onfailure = func.onfailure
307 306 else:
308 307 func = _xmerge
309 308 trymerge = True
310 309 onfailure = _("merging %s failed!\n")
311 310
312 311 toolconf = tool, toolpath, binary, symlink
313 312
314 313 if not trymerge:
315 314 return func(repo, mynode, orig, fcd, fco, fca, toolconf)
316 315
317 316 a = repo.wjoin(fd)
318 317 b = temp("base", fca)
319 318 c = temp("other", fco)
320 319 back = a + ".orig"
321 320 util.copyfile(a, back)
322 321
323 322 if orig != fco.path():
324 323 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
325 324 else:
326 325 ui.status(_("merging %s\n") % fd)
327 326
328 327 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
329 328
329 labels = ['local', 'other']
330 330 needcheck, r = func(repo, mynode, orig, fcd, fco, fca, toolconf,
331 (a, b, c, back))
331 (a, b, c, back), labels=labels)
332 332 if not needcheck:
333 333 if r:
334 334 if onfailure:
335 335 ui.warn(onfailure % fd)
336 336 else:
337 337 util.unlink(back)
338 338
339 339 util.unlink(b)
340 340 util.unlink(c)
341 341 return r
342 342
343 343 if not r and (_toolbool(ui, tool, "checkconflicts") or
344 344 'conflicts' in _toollist(ui, tool, "check")):
345 345 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
346 346 re.MULTILINE):
347 347 r = 1
348 348
349 349 checked = False
350 350 if 'prompt' in _toollist(ui, tool, "check"):
351 351 checked = True
352 352 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
353 353 "$$ &Yes $$ &No") % fd, 1):
354 354 r = 1
355 355
356 356 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
357 357 'changed' in _toollist(ui, tool, "check")):
358 358 if filecmp.cmp(a, back):
359 359 if ui.promptchoice(_(" output file %s appears unchanged\n"
360 360 "was merge successful (yn)?"
361 361 "$$ &Yes $$ &No") % fd, 1):
362 362 r = 1
363 363
364 364 if _toolbool(ui, tool, "fixeol"):
365 365 _matcheol(a, back)
366 366
367 367 if r:
368 368 if onfailure:
369 369 ui.warn(onfailure % fd)
370 370 else:
371 371 util.unlink(back)
372 372
373 373 util.unlink(b)
374 374 util.unlink(c)
375 375 return r
376 376
377 377 # tell hggettext to extract docstrings from these functions:
378 378 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now