##// END OF EJS Templates
filemerge: use only the first line of the generated conflict marker for safety...
FUJIWARA Katsunori -
r21864:755bf1bb default
parent child Browse files
Show More
@@ -1,437 +1,440
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, templater, templatekw
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 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 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 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 214 r = _premerge(repo, toolconf, files, labels=labels)
215 215 if r:
216 216 a, b, c, back = files
217 217
218 218 ui = repo.ui
219 219
220 220 r = simplemerge.simplemerge(ui, a, b, c, label=labels)
221 221 return True, r
222 222 return False, 0
223 223
224 224 @internaltool('dump', True)
225 225 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
226 226 """
227 227 Creates three versions of the files to merge, containing the
228 228 contents of local, other and base. These files can then be used to
229 229 perform a merge manually. If the file to be merged is named
230 230 ``a.txt``, these files will accordingly be named ``a.txt.local``,
231 231 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
232 232 same directory as ``a.txt``."""
233 233 r = _premerge(repo, toolconf, files, labels=labels)
234 234 if r:
235 235 a, b, c, back = files
236 236
237 237 fd = fcd.path()
238 238
239 239 util.copyfile(a, a + ".local")
240 240 repo.wwrite(fd + ".other", fco.data(), fco.flags())
241 241 repo.wwrite(fd + ".base", fca.data(), fca.flags())
242 242 return False, r
243 243
244 244 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
245 245 r = _premerge(repo, toolconf, files, labels=labels)
246 246 if r:
247 247 tool, toolpath, binary, symlink = toolconf
248 248 a, b, c, back = files
249 249 out = ""
250 250 env = {'HG_FILE': fcd.path(),
251 251 'HG_MY_NODE': short(mynode),
252 252 'HG_OTHER_NODE': str(fco.changectx()),
253 253 'HG_BASE_NODE': str(fca.changectx()),
254 254 'HG_MY_ISLINK': 'l' in fcd.flags(),
255 255 'HG_OTHER_ISLINK': 'l' in fco.flags(),
256 256 'HG_BASE_ISLINK': 'l' in fca.flags(),
257 257 }
258 258
259 259 ui = repo.ui
260 260
261 261 args = _toolstr(ui, tool, "args", '$local $base $other')
262 262 if "$output" in args:
263 263 out, a = a, back # read input from backup, write to original
264 264 replace = {'local': a, 'base': b, 'other': c, 'output': out}
265 265 args = util.interpolate(r'\$', replace, args,
266 266 lambda s: util.shellquote(util.localpath(s)))
267 267 r = util.system(toolpath + ' ' + args, cwd=repo.root, environ=env,
268 268 out=ui.fout)
269 269 return True, r
270 270 return False, 0
271 271
272 272 def _formatconflictmarker(repo, ctx, template, label, pad):
273 273 """Applies the given template to the ctx, prefixed by the label.
274 274
275 275 Pad is the minimum width of the label prefix, so that multiple markers
276 276 can have aligned templated parts.
277 277 """
278 278 if ctx.node() is None:
279 279 ctx = ctx.p1()
280 280
281 281 props = templatekw.keywords.copy()
282 282 props['templ'] = template
283 283 props['ctx'] = ctx
284 284 props['repo'] = repo
285 285 templateresult = template('conflictmarker', **props)
286 286
287 287 label = ('%s:' % label).ljust(pad + 1)
288 288 mark = '%s %s' % (label, templater.stringify(templateresult))
289 289
290 if mark:
291 mark = mark.splitlines()[0] # split for safety
292
290 293 # The <<< marks add 8 to the length, and '...' adds three, so max
291 294 # length of the actual marker is 69.
292 295 maxlength = 80 - 8 - 3
293 296 if len(mark) > maxlength:
294 297 mark = mark[:maxlength] + '...'
295 298 return mark
296 299
297 300 _defaultconflictmarker = ('{node|short} ' +
298 301 '{ifeq(tags, "tip", "", "{tags} ")}' +
299 302 '{if(bookmarks, "{bookmarks} ")}' +
300 303 '{ifeq(branch, "default", "", "{branch} ")}' +
301 304 '- {author|user}: {desc|firstline}')
302 305
303 306 _defaultconflictlabels = ['local', 'other']
304 307
305 308 def _formatlabels(repo, fcd, fco, labels):
306 309 """Formats the given labels using the conflict marker template.
307 310
308 311 Returns a list of formatted labels.
309 312 """
310 313 cd = fcd.changectx()
311 314 co = fco.changectx()
312 315
313 316 ui = repo.ui
314 317 template = ui.config('ui', 'mergemarkertemplate', _defaultconflictmarker)
315 318 template = templater.parsestring(template, quoted=False)
316 319 tmpl = templater.templater(None, cache={ 'conflictmarker' : template })
317 320
318 321 pad = max(len(labels[0]), len(labels[1]))
319 322
320 323 return [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
321 324 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
322 325
323 326 def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
324 327 """perform a 3-way merge in the working directory
325 328
326 329 mynode = parent node before merge
327 330 orig = original local filename before merge
328 331 fco = other file context
329 332 fca = ancestor file context
330 333 fcd = local file context for current/destination file
331 334 """
332 335
333 336 def temp(prefix, ctx):
334 337 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
335 338 (fd, name) = tempfile.mkstemp(prefix=pre)
336 339 data = repo.wwritedata(ctx.path(), ctx.data())
337 340 f = os.fdopen(fd, "wb")
338 341 f.write(data)
339 342 f.close()
340 343 return name
341 344
342 345 if not fco.cmp(fcd): # files identical?
343 346 return None
344 347
345 348 ui = repo.ui
346 349 fd = fcd.path()
347 350 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
348 351 symlink = 'l' in fcd.flags() + fco.flags()
349 352 tool, toolpath = _picktool(repo, ui, fd, binary, symlink)
350 353 ui.debug("picked tool '%s' for %s (binary %s symlink %s)\n" %
351 354 (tool, fd, binary, symlink))
352 355
353 356 if tool in internals:
354 357 func = internals[tool]
355 358 trymerge = func.trymerge
356 359 onfailure = func.onfailure
357 360 else:
358 361 func = _xmerge
359 362 trymerge = True
360 363 onfailure = _("merging %s failed!\n")
361 364
362 365 toolconf = tool, toolpath, binary, symlink
363 366
364 367 if not trymerge:
365 368 return func(repo, mynode, orig, fcd, fco, fca, toolconf)
366 369
367 370 a = repo.wjoin(fd)
368 371 b = temp("base", fca)
369 372 c = temp("other", fco)
370 373 back = a + ".orig"
371 374 util.copyfile(a, back)
372 375
373 376 if orig != fco.path():
374 377 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
375 378 else:
376 379 ui.status(_("merging %s\n") % fd)
377 380
378 381 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
379 382
380 383 markerstyle = ui.config('ui', 'mergemarkers', 'detailed')
381 384 if markerstyle == 'basic':
382 385 formattedlabels = _defaultconflictlabels
383 386 else:
384 387 if not labels:
385 388 labels = _defaultconflictlabels
386 389
387 390 formattedlabels = _formatlabels(repo, fcd, fco, labels)
388 391
389 392 needcheck, r = func(repo, mynode, orig, fcd, fco, fca, toolconf,
390 393 (a, b, c, back), labels=formattedlabels)
391 394 if not needcheck:
392 395 if r:
393 396 if onfailure:
394 397 ui.warn(onfailure % fd)
395 398 else:
396 399 util.unlink(back)
397 400
398 401 util.unlink(b)
399 402 util.unlink(c)
400 403 return r
401 404
402 405 if not r and (_toolbool(ui, tool, "checkconflicts") or
403 406 'conflicts' in _toollist(ui, tool, "check")):
404 407 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
405 408 re.MULTILINE):
406 409 r = 1
407 410
408 411 checked = False
409 412 if 'prompt' in _toollist(ui, tool, "check"):
410 413 checked = True
411 414 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
412 415 "$$ &Yes $$ &No") % fd, 1):
413 416 r = 1
414 417
415 418 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
416 419 'changed' in _toollist(ui, tool, "check")):
417 420 if filecmp.cmp(a, back):
418 421 if ui.promptchoice(_(" output file %s appears unchanged\n"
419 422 "was merge successful (yn)?"
420 423 "$$ &Yes $$ &No") % fd, 1):
421 424 r = 1
422 425
423 426 if _toolbool(ui, tool, "fixeol"):
424 427 _matcheol(a, back)
425 428
426 429 if r:
427 430 if onfailure:
428 431 ui.warn(onfailure % fd)
429 432 else:
430 433 util.unlink(back)
431 434
432 435 util.unlink(b)
433 436 util.unlink(c)
434 437 return r
435 438
436 439 # tell hggettext to extract docstrings from these functions:
437 440 i18nfunctions = internals.values()
@@ -1,73 +1,93
1 1 $ hg init
2 2 $ echo "nothing" > a
3 3 $ hg add a
4 4 $ hg commit -m ancestor
5 5 $ echo "something" > a
6 6 $ hg commit -m branch1
7 7 $ hg co 0
8 8 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
9 9 $ echo "something else" > a
10 10 $ hg commit -m branch2
11 11 created new head
12 12
13 13 $ hg merge 1
14 14 merging a
15 15 warning: conflicts during merge.
16 16 merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
17 17 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
18 18 use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
19 19 [1]
20 20
21 21 $ hg id
22 22 32e80765d7fe+75234512624c+ tip
23 23
24 24 $ cat a
25 25 <<<<<<< local: 32e80765d7fe - test: branch2
26 26 something else
27 27 =======
28 28 something
29 29 >>>>>>> other: 75234512624c - test: branch1
30 30
31 31 $ hg status
32 32 M a
33 33 ? a.orig
34 34
35 35 Verify custom conflict markers
36 36
37 37 $ hg up -q --clean .
38 38 $ printf "\n[ui]\nmergemarkertemplate={author} {rev}\n" >> .hg/hgrc
39 39
40 40 $ hg merge 1
41 41 merging a
42 42 warning: conflicts during merge.
43 43 merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
44 44 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
45 45 use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
46 46 [1]
47 47
48 48 $ cat a
49 49 <<<<<<< local: test 2
50 50 something else
51 51 =======
52 52 something
53 53 >>>>>>> other: test 1
54 54
55 Verify line splitting of custom conflict marker which causes multiple lines
56
57 $ hg up -q --clean .
58 $ cat >> .hg/hgrc <<EOF
59 > [ui]
60 > mergemarkertemplate={author} {rev}\nfoo\nbar\nbaz
61 > EOF
62
63 $ hg -q merge 1
64 warning: conflicts during merge.
65 merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
66 [1]
67
68 $ cat a
69 <<<<<<< local: test 2
70 something else
71 =======
72 something
73 >>>>>>> other: test 1
74
55 75 Verify basic conflict markers
56 76
57 77 $ hg up -q --clean .
58 78 $ printf "\n[ui]\nmergemarkers=basic\n" >> .hg/hgrc
59 79
60 80 $ hg merge 1
61 81 merging a
62 82 warning: conflicts during merge.
63 83 merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
64 84 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
65 85 use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
66 86 [1]
67 87
68 88 $ cat a
69 89 <<<<<<< local
70 90 something else
71 91 =======
72 92 something
73 93 >>>>>>> other
General Comments 0
You need to be logged in to leave comments. Login now