PageRenderTime 48ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/mercurial/filemerge.py

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