/tortoisehg/hgqt/wctxactions.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 330 lines · 287 code · 30 blank · 13 comment · 64 complexity · 859bdff4831fa191e0d12dc73d97e33d MD5 · raw file

  1. # wctxactions.py - menu and responses for working copy files
  2. #
  3. # Copyright 2010 Steve Borho <steve@borho.org>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2, incorporated herein by reference.
  7. import os
  8. import re
  9. import subprocess
  10. from mercurial import util, error, merge, commands
  11. from tortoisehg.hgqt import qtlib, htmlui, visdiff
  12. from tortoisehg.util import hglib, shlib
  13. from tortoisehg.hgqt.i18n import _
  14. from PyQt4.QtCore import Qt
  15. from PyQt4.QtGui import QAction, QMenu, QMessageBox, QFileDialog, QDialog
  16. def wctxactions(parent, point, repo, selrows):
  17. if not selrows:
  18. return
  19. alltypes = set()
  20. for t, path in selrows:
  21. alltypes |= t
  22. def make(text, func, types, icon=None):
  23. files = [f for t, f in selrows if t & types]
  24. if not files:
  25. return None
  26. action = menu.addAction(text)
  27. if icon:
  28. action.setIcon(icon)
  29. action.args = (func, parent, files, repo)
  30. action.run = lambda: run(*action.args)
  31. action.triggered.connect(action.run)
  32. return action
  33. if hasattr(parent, 'contextmenu'):
  34. menu = parent.contextmenu
  35. menu.clear()
  36. else:
  37. menu = QMenu(parent)
  38. parent.contextmenu = menu
  39. make(_('&Visual Diff'), vdiff, frozenset('MAR!'))
  40. make(_('Edit'), edit, frozenset('MACI?'))
  41. make(_('View missing'), viewmissing, frozenset('R!'))
  42. if len(repo.parents()) > 1:
  43. make(_('View other'), viewother, frozenset('MA'))
  44. menu.addSeparator()
  45. make(_('&Revert'), revert, frozenset('MAR!'))
  46. make(_('&Add'), add, frozenset('R'))
  47. menu.addSeparator()
  48. make(_('File History'), log, frozenset('MARC!'))
  49. make(_('&Annotate'), annotate, frozenset('MARC!'))
  50. menu.addSeparator()
  51. make(_('&Forget'), forget, frozenset('MAC!'))
  52. make(_('&Add'), add, frozenset('I?'))
  53. make(_('&Detect Renames...'), guessRename, frozenset('A?!'))
  54. make(_('&Ignore'), ignore, frozenset('?'))
  55. make(_('Remove versioned'), remove, frozenset('C'))
  56. make(_('&Delete unversioned'), delete, frozenset('?I'))
  57. if len(selrows) == 1:
  58. menu.addSeparator()
  59. t, path = selrows[0]
  60. wctx = repo[None]
  61. if t & frozenset('?') and wctx.deleted():
  62. rmenu = QMenu(_('Was renamed from'))
  63. for d in wctx.deleted()[:15]:
  64. def mkaction(deleted):
  65. a = rmenu.addAction(hglib.tounicode(deleted))
  66. a.triggered.connect(lambda: renamefromto(repo, deleted, path))
  67. mkaction(d)
  68. menu.addMenu(rmenu)
  69. else:
  70. make(_('&Copy...'), copy, frozenset('MC'))
  71. make(_('Rename...'), rename, frozenset('MC'))
  72. menu.addSeparator()
  73. make(_('Mark unresolved'), unmark, frozenset('r'))
  74. make(_('Mark resolved'), mark, frozenset('u'))
  75. f = make(_('Restart Merge...'), resolve, frozenset('u'))
  76. if f:
  77. files = [f for t, f in selrows if 'u' in t]
  78. rmenu = QMenu(_('Restart merge with'))
  79. for tool in hglib.mergetools(repo.ui):
  80. def mkaction(rtool):
  81. a = rmenu.addAction(hglib.tounicode(rtool))
  82. a.triggered.connect(lambda: resolve_with(rtool, repo, files))
  83. mkaction(tool)
  84. menu.addMenu(rmenu)
  85. return menu.exec_(point)
  86. def run(func, parent, files, repo):
  87. 'run wrapper for all action methods'
  88. hu = htmlui.htmlui()
  89. name = func.__name__.title()
  90. notify = False
  91. cwd = os.getcwd()
  92. try:
  93. os.chdir(repo.root)
  94. try:
  95. # All operations should quietly succeed. Any error should
  96. # result in a message box
  97. notify = func(parent, hu, repo, files)
  98. o, e = hu.getdata()
  99. if e:
  100. QMessageBox.warning(parent, name + _(' errors'), str(e))
  101. elif o:
  102. QMessageBox.information(parent, name + _(' output'), str(o))
  103. elif notify:
  104. wfiles = [repo.wjoin(x) for x in files]
  105. shlib.shell_notify(wfiles)
  106. except (IOError, OSError), e:
  107. err = hglib.tounicode(str(e))
  108. QMessageBox.critical(parent, name + _(' Aborted'), err)
  109. except util.Abort, e:
  110. if e.hint:
  111. err = _('%s (hint: %s)') % (hglib.tounicode(str(e)),
  112. hglib.tounicode(e.hint))
  113. else:
  114. err = hglib.tounicode(str(e))
  115. QMessageBox.critical(parent, name + _(' Aborted'), err)
  116. except (error.LookupError), e:
  117. err = hglib.tounicode(str(e))
  118. QMessageBox.critical(parent, name + _(' Aborted'), err)
  119. except NotImplementedError:
  120. QMessageBox.critical(parent, name + _(' not implemented'),
  121. 'Please add it :)')
  122. finally:
  123. os.chdir(cwd)
  124. return notify
  125. def renamefromto(repo, deleted, unknown):
  126. repo[None].copy(deleted, unknown)
  127. repo[None].remove([deleted], unlink=False) # !->R
  128. def vdiff(parent, ui, repo, files):
  129. dlg = visdiff.visualdiff(ui, repo, files, {})
  130. if dlg:
  131. dlg.exec_()
  132. def edit(parent, ui, repo, files, lineno=None, search=None):
  133. files = [util.shellquote(util.localpath(f)) for f in files]
  134. editor = ui.config('tortoisehg', 'editor')
  135. assert len(files) == 1 or lineno == None
  136. if editor:
  137. try:
  138. regexp = re.compile('\[([^\]]*)\]')
  139. expanded = []
  140. pos = 0
  141. for m in regexp.finditer(editor):
  142. expanded.append(editor[pos:m.start()-1])
  143. phrase = editor[m.start()+1:m.end()-1]
  144. pos=m.end()+1
  145. if '$LINENUM' in phrase:
  146. if lineno is None:
  147. # throw away phrase
  148. continue
  149. phrase = phrase.replace('$LINENUM', str(lineno))
  150. elif '$SEARCH' in phrase:
  151. if search is None:
  152. # throw away phrase
  153. continue
  154. phrase = phrase.replace('$SEARCH', search)
  155. if '$FILE' in phrase:
  156. phrase = phrase.replace('$FILE', files[0])
  157. files = []
  158. expanded.append(phrase)
  159. expanded.append(editor[pos:])
  160. cmdline = ' '.join(expanded + files)
  161. except ValueError, e:
  162. # '[' or ']' not found
  163. cmdline = ' '.join([editor] + files)
  164. except TypeError, e:
  165. # variable expansion failed
  166. cmdline = ' '.join([editor] + files)
  167. else:
  168. editor = os.environ.get('HGEDITOR') or ui.config('ui', 'editor') or \
  169. os.environ.get('EDITOR', 'vi')
  170. cmdline = ' '.join([editor] + files)
  171. if os.path.basename(editor) in ('vi', 'vim', 'hgeditor'):
  172. res = QMessageBox.critical(parent,
  173. _('No visual editor configured'),
  174. _('Please configure a visual editor.'))
  175. from tortoisehg.hgqt.settings import SettingsDialog
  176. dlg = SettingsDialog(False, focus='tortoisehg.editor')
  177. dlg.exec_()
  178. return
  179. cmdline = util.quotecommand(cmdline)
  180. try:
  181. subprocess.Popen(cmdline, shell=True, creationflags=visdiff.openflags,
  182. stderr=None, stdout=None, stdin=None)
  183. except (OSError, EnvironmentError), e:
  184. QMessageBox.warning(parent,
  185. _('Editor launch failure'),
  186. _('%s : %s') % (cmd, str(e)))
  187. return False
  188. def viewmissing(parent, ui, repo, files):
  189. base, _ = visdiff.snapshot(repo, files, repo['.'])
  190. edit(parent, ui, repo, [os.path.join(base, f) for f in files])
  191. def viewother(parent, ui, repo, files):
  192. wctx = repo[None]
  193. assert bool(wctx.p2())
  194. base, _ = visdiff.snapshot(repo, files, wctx.p2())
  195. edit(parent, ui, repo, [os.path.join(base, f) for f in files])
  196. def revert(parent, ui, repo, files):
  197. revertopts = {'date': None, 'rev': '.'}
  198. if len(repo.parents()) > 1:
  199. res = qtlib.CustomPrompt(
  200. _('Uncommited merge - please select a parent revision'),
  201. _('Revert files to local or other parent?'), parent,
  202. (_('&Local'), _('&Other'), _('Cancel')), 0, 2, files).run()
  203. if res == 0:
  204. revertopts['rev'] = repo[None].p1().rev()
  205. elif res == 1:
  206. revertopts['rev'] = repo[None].p2().rev()
  207. else:
  208. return
  209. commands.revert(ui, repo, *files, **revertopts)
  210. else:
  211. res = qtlib.CustomPrompt(
  212. _('Confirm Revert'),
  213. _('Revert changes to files?'), parent,
  214. (_('&Yes (backup changes)'), _('Yes (&discard changes)'),
  215. _('Cancel')), 2, 2, files).run()
  216. if res == 2:
  217. return False
  218. if res == 1:
  219. revertopts['no_backup'] = True
  220. commands.revert(ui, repo, *files, **revertopts)
  221. return True
  222. def log(parent, ui, repo, files):
  223. from tortoisehg.hgqt.workbench import run
  224. from tortoisehg.hgqt.run import qtrun
  225. opts = {'root': repo.root}
  226. qtrun(run, repo.ui, *files, **opts)
  227. return False
  228. def annotate(parent, ui, repo, files):
  229. from tortoisehg.hgqt.annotate import run
  230. from tortoisehg.hgqt.run import qtrun
  231. opts = {'root': repo.root}
  232. qtrun(run, repo.ui, *files, **opts)
  233. return False
  234. def forget(parent, ui, repo, files):
  235. commands.forget(ui, repo, *files)
  236. return True
  237. def add(parent, ui, repo, files):
  238. commands.add(ui, repo, *files)
  239. return True
  240. def guessRename(parent, ui, repo, files):
  241. from tortoisehg.hgqt.guess import DetectRenameDialog
  242. dlg = DetectRenameDialog(repo, parent, *files)
  243. def matched():
  244. ret = True
  245. ret = False
  246. dlg.matchAccepted.connect(matched)
  247. dlg.finished.connect(dlg.deleteLater)
  248. dlg.exec_()
  249. return ret
  250. def ignore(parent, ui, repo, files):
  251. from tortoisehg.hgqt.hgignore import HgignoreDialog
  252. dlg = HgignoreDialog(repo, parent, *files)
  253. dlg.finished.connect(dlg.deleteLater)
  254. return dlg.exec_() == QDialog.Accepted
  255. def remove(parent, ui, repo, files):
  256. commands.remove(ui, repo, *files)
  257. return True
  258. def delete(parent, ui, repo, files):
  259. res = qtlib.CustomPrompt(
  260. _('Confirm Delete Unrevisioned'),
  261. _('Delete the following unrevisioned files?'),
  262. parent, (_('&Delete'), _('Cancel')), 1, 1, files).run()
  263. if res == 1:
  264. return
  265. for wfile in files:
  266. os.unlink(wfile)
  267. return True
  268. def copy(parent, ui, repo, files):
  269. assert len(files) == 1
  270. wfile = repo.wjoin(files[0])
  271. fd = QFileDialog(parent)
  272. fname = fd.getSaveFileName(parent, _('Copy file to'), wfile)
  273. if not fname:
  274. return
  275. fname = hglib.fromunicode(fname)
  276. wfiles = [wfile, fname]
  277. commands.copy(ui, repo, *wfiles)
  278. return True
  279. def rename(parent, ui, repo, files):
  280. # needs rename dialog
  281. raise NotImplementedError()
  282. def unmark(parent, ui, repo, files):
  283. ms = merge.mergestate(repo)
  284. for wfile in files:
  285. ms.mark(wfile, 'u')
  286. ms.commit()
  287. return True
  288. def mark(parent, ui, repo, files):
  289. ms = merge.mergestate(repo)
  290. for wfile in files:
  291. ms.mark(wfile, 'r')
  292. ms.commit()
  293. return True
  294. def resolve(parent, ui, repo, files):
  295. commands.resolve(ui, repo, *files)
  296. return True
  297. def resolve_with(tool, repo, files):
  298. opts = {'tool': tool}
  299. commands.resolve(repo.ui, repo, *files, **opts)
  300. return True