/contrib/nautilus-thg.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 380 lines · 309 code · 43 blank · 28 comment · 88 complexity · 0b1b273e04cbbe35c4de2b8598973377 MD5 · raw file

  1. # TortoiseHg plugin for Nautilus
  2. #
  3. # Copyright 2007 Steve Borho
  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 gtk
  8. import gobject
  9. import nautilus
  10. import gnomevfs
  11. import os
  12. import sys
  13. thg_main = 'thg'
  14. idstr_prefix = 'HgNautilus2'
  15. if gtk.gtk_version < (2, 14, 0):
  16. # at least on 2.12.12, gtk widgets can be confused by control
  17. # char markups (like "&#x1;"), so use cgi.escape instead
  18. from cgi import escape as markup_escape_text
  19. else:
  20. from gobject import markup_escape_text
  21. try:
  22. from mercurial import demandimport
  23. except ImportError:
  24. # workaround to use user's local python libs
  25. userlib = os.path.expanduser('~/lib/python')
  26. if os.path.exists(userlib) and userlib not in sys.path:
  27. sys.path.append(userlib)
  28. from mercurial import demandimport
  29. demandimport.enable()
  30. import subprocess
  31. import urllib
  32. from mercurial import hg, ui, match, util, error
  33. from mercurial.node import short
  34. def _thg_path():
  35. # check if nautilus-thg.py is a symlink first
  36. pfile = __file__
  37. if pfile.endswith('.pyc'):
  38. pfile = pfile[:-1]
  39. path = os.path.dirname(os.path.dirname(os.path.realpath(pfile)))
  40. thgpath = os.path.normpath(path)
  41. testpath = os.path.join(thgpath, 'tortoisehg')
  42. if os.path.isdir(testpath) and thgpath not in sys.path:
  43. sys.path.insert(0, thgpath)
  44. _thg_path()
  45. from tortoisehg.util import paths, debugthg, cachethg
  46. if debugthg.debug('N'):
  47. debugf = debugthg.debugf
  48. else:
  49. debugf = debugthg.debugf_No
  50. nofilecmds = 'about serve synch repoconfig userconfig merge unmerge'.split()
  51. class HgExtensionDefault:
  52. def __init__(self):
  53. self.scanStack = []
  54. self.allvfs = {}
  55. self.inv_dirs = set()
  56. from tortoisehg.util import menuthg
  57. self.hgtk = paths.find_in_path(thg_main)
  58. self.menu = menuthg.menuThg()
  59. self.notify = os.path.expanduser('~/.tortoisehg/notify')
  60. try:
  61. f = open(self.notify, 'w')
  62. f.close()
  63. ds_uri = gnomevfs.get_uri_from_local_path(self.notify)
  64. self.gmon = gnomevfs.monitor_add(ds_uri,
  65. gnomevfs.MONITOR_FILE, self.notified)
  66. except (gnomevfs.NotSupportedError, IOError), e:
  67. debugf('no notification because of %s', e)
  68. self.notify = ''
  69. def icon(self, iname):
  70. return paths.get_tortoise_icon(iname)
  71. def get_path_for_vfs_file(self, vfs_file, store=True):
  72. if vfs_file.get_uri_scheme() != 'file':
  73. return None
  74. path = urllib.unquote(vfs_file.get_uri()[7:])
  75. if vfs_file.is_gone():
  76. self.allvfs.pop(path, '')
  77. return None
  78. if store:
  79. self.allvfs[path] = vfs_file
  80. return path
  81. def get_vfs(self, path):
  82. vfs = self.allvfs.get(path, None)
  83. if vfs and vfs.is_gone():
  84. del self.allvfs[path]
  85. return None
  86. return vfs
  87. def get_repo_for_path(self, path):
  88. '''
  89. Find mercurial repository for vfs_file
  90. Returns hg.repo
  91. '''
  92. p = paths.find_root(path)
  93. if not p:
  94. return None
  95. try:
  96. return hg.repository(ui.ui(), path=p)
  97. except error.RepoError:
  98. return None
  99. except StandardError, e:
  100. debugf(e)
  101. return None
  102. def run_dialog(self, menuitem, hgtkcmd, cwd = None):
  103. '''
  104. hgtkcmd - hgtk subcommand
  105. '''
  106. if cwd: #bg
  107. self.files = []
  108. else:
  109. cwd = self.cwd
  110. repo = self.get_repo_for_path(cwd)
  111. cmdopts = [sys.executable, self.hgtk, hgtkcmd]
  112. if hgtkcmd not in nofilecmds and self.files:
  113. pipe = subprocess.PIPE
  114. cmdopts += ['--listfile', '-']
  115. else:
  116. pipe = None
  117. proc = subprocess.Popen(cmdopts, cwd=cwd, stdin=pipe, shell=False)
  118. if pipe:
  119. proc.stdin.write('\n'.join(self.files))
  120. proc.stdin.close()
  121. def buildMenu(self, vfs_files, bg):
  122. '''Build menu'''
  123. self.pos = 0
  124. self.files = []
  125. files = []
  126. for vfs_file in vfs_files:
  127. f = self.get_path_for_vfs_file(vfs_file)
  128. if f:
  129. files.append(f)
  130. if not files:
  131. return
  132. if bg or len(files) == 1 and vfs_files[0].is_directory():
  133. cwd = files[0]
  134. files = []
  135. else:
  136. cwd = os.path.dirname(files[0])
  137. repo = self.get_repo_for_path(cwd)
  138. if repo:
  139. menus = self.menu.get_commands(repo, cwd, files)
  140. self.files = files
  141. else:
  142. menus = self.menu.get_norepo_commands(cwd, files)
  143. self.cwd = cwd
  144. return self._buildMenu(menus)
  145. def _buildMenu(self, menus):
  146. '''Build one level of a menu'''
  147. items = []
  148. if self.files:
  149. passcwd = None
  150. else: #bg
  151. passcwd = self.cwd
  152. for menu_info in menus:
  153. idstr = '%s::%02d%s' % (idstr_prefix ,self.pos, menu_info.hgcmd)
  154. self.pos += 1
  155. if menu_info.isSep():
  156. # can not insert a separator till now
  157. pass
  158. elif menu_info.isSubmenu():
  159. if hasattr(nautilus, 'Menu'):
  160. item = nautilus.MenuItem(idstr, menu_info.menutext,
  161. menu_info.helptext)
  162. submenu = nautilus.Menu()
  163. item.set_submenu(submenu)
  164. for subitem in self._buildMenu(menu_info.get_menus()):
  165. submenu.append_item(subitem)
  166. items.append(item)
  167. else: #submenu not suported
  168. for subitem in self._buildMenu(menu_info.get_menus()):
  169. items.append(subitem)
  170. else:
  171. if menu_info.state:
  172. item = nautilus.MenuItem(idstr,
  173. menu_info.menutext,
  174. menu_info.helptext,
  175. self.icon(menu_info.icon))
  176. item.connect('activate', self.run_dialog, menu_info.hgcmd,
  177. passcwd)
  178. items.append(item)
  179. return items
  180. def get_background_items(self, window, vfs_file):
  181. '''Build context menu for current directory'''
  182. if vfs_file and self.menu:
  183. return self.buildMenu([vfs_file], True)
  184. else:
  185. self.files = []
  186. def get_file_items(self, window, vfs_files):
  187. '''Build context menu for selected files/directories'''
  188. if vfs_files and self.menu:
  189. return self.buildMenu(vfs_files, False)
  190. def get_columns(self):
  191. return nautilus.Column(idstr_prefix + "::80hg_status",
  192. "hg_status",
  193. "Hg Status",
  194. "Version control status"),
  195. def _get_file_status(self, localpath, repo=None):
  196. cachestate = cachethg.get_state(localpath, repo)
  197. cache2state = {cachethg.UNCHANGED: ('default', 'clean'),
  198. cachethg.ADDED: ('new', 'added'),
  199. cachethg.MODIFIED: ('important', 'modified'),
  200. cachethg.UNKNOWN: (None, 'unrevisioned'),
  201. cachethg.IGNORED: ('noread', 'ignored'),
  202. cachethg.NOT_IN_REPO: (None, 'unrevisioned'),
  203. cachethg.ROOT: ('generic', 'root'),
  204. cachethg.UNRESOLVED: ('danger', 'unresolved')}
  205. emblem, status = cache2state.get(cachestate, (None, '?'))
  206. return emblem, status
  207. def notified(self, mon_uri=None, event_uri=None, event=None):
  208. debugf('notified from hgtk, %s', event, level='n')
  209. f = open(self.notify, 'a+')
  210. files = None
  211. try:
  212. files = [line[:-1] for line in f if line]
  213. if files:
  214. f.truncate(0)
  215. finally:
  216. f.close()
  217. if not files:
  218. return
  219. root = os.path.commonprefix(files)
  220. root = paths.find_root(root)
  221. if root:
  222. self.invalidate(files, root)
  223. def invalidate(self, paths, root = ''):
  224. started = bool(self.inv_dirs)
  225. if cachethg.cache_pdir == root and root not in self.inv_dirs:
  226. cachethg.overlay_cache.clear()
  227. self.inv_dirs.update([os.path.dirname(root), '/', ''])
  228. for path in paths:
  229. path = os.path.join(root, path)
  230. while path not in self.inv_dirs:
  231. self.inv_dirs.add(path)
  232. path = os.path.dirname(path)
  233. if cachethg.cache_pdir == path:
  234. cachethg.overlay_cache.clear()
  235. if started:
  236. return
  237. if len(paths) > 1:
  238. self._invalidate_dirs()
  239. else:
  240. #group invalidation of directories
  241. gobject.timeout_add(200, self._invalidate_dirs)
  242. def _invalidate_dirs(self):
  243. for path in self.inv_dirs:
  244. vfs = self.get_vfs(path)
  245. if vfs:
  246. vfs.invalidate_extension_info()
  247. self.inv_dirs.clear()
  248. # property page borrowed from http://www.gnome.org/~gpoo/hg/nautilus-hg/
  249. def __add_row(self, row, label_item, label_value):
  250. label = gtk.Label(label_item)
  251. label.set_use_markup(True)
  252. label.set_alignment(1, 0)
  253. self.table.attach(label, 0, 1, row, row + 1, gtk.FILL, gtk.FILL, 0, 0)
  254. label.show()
  255. label = gtk.Label(label_value)
  256. label.set_use_markup(True)
  257. label.set_alignment(0, 1)
  258. label.show()
  259. self.table.attach(label, 1, 2, row, row + 1, gtk.FILL, 0, 0, 0)
  260. def get_property_pages(self, vfs_files):
  261. if len(vfs_files) != 1:
  262. return
  263. file = vfs_files[0]
  264. path = self.get_path_for_vfs_file(file)
  265. if path is None or file.is_directory():
  266. return
  267. repo = self.get_repo_for_path(path)
  268. if repo is None:
  269. return
  270. localpath = path[len(repo.root)+1:]
  271. emblem, status = self._get_file_status(path, repo)
  272. # Get the information from Mercurial
  273. ctx = repo['.']
  274. try:
  275. fctx = ctx.filectx(localpath)
  276. rev = fctx.filelog().linkrev(fctx.filerev())
  277. except:
  278. rev = ctx.rev()
  279. ctx = repo.changectx(rev)
  280. node = short(ctx.node())
  281. date = util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2')
  282. parents = '\n'.join([short(p.node()) for p in ctx.parents()])
  283. description = ctx.description()
  284. user = ctx.user()
  285. user = markup_escape_text(user)
  286. tags = ', '.join(ctx.tags())
  287. branch = ctx.branch()
  288. self.property_label = gtk.Label('Mercurial')
  289. self.table = gtk.Table(7, 2, False)
  290. self.table.set_border_width(5)
  291. self.table.set_row_spacings(5)
  292. self.table.set_col_spacings(5)
  293. self.__add_row(0, '<b>Status</b>:', status)
  294. self.__add_row(1, '<b>Last-Commit-Revision</b>:', str(rev))
  295. self.__add_row(2, '<b>Last-Commit-Description</b>:', description)
  296. self.__add_row(3, '<b>Last-Commit-Date</b>:', date)
  297. self.__add_row(4, '<b>Last-Commit-User</b>:', user)
  298. if tags:
  299. self.__add_row(5, '<b>Tags</b>:', tags)
  300. if branch != 'default':
  301. self.__add_row(6, '<b>Branch</b>:', branch)
  302. self.table.show()
  303. return nautilus.PropertyPage("MercurialPropertyPage::status",
  304. self.property_label, self.table),
  305. class HgExtensionIcons(HgExtensionDefault):
  306. def update_file_info(self, file):
  307. '''Queue file for emblem and hg status update'''
  308. self.scanStack.append(file)
  309. if len(self.scanStack) == 1:
  310. gobject.idle_add(self.fileinfo_on_idle)
  311. def fileinfo_on_idle(self):
  312. '''Update emblem and hg status for files when there is time'''
  313. if not self.scanStack:
  314. return False
  315. try:
  316. vfs_file = self.scanStack.pop()
  317. path = self.get_path_for_vfs_file(vfs_file, False)
  318. if not path:
  319. return True
  320. oldvfs = self.get_vfs(path)
  321. if oldvfs and oldvfs != vfs_file:
  322. #file has changed on disc (not invalidated)
  323. self.get_path_for_vfs_file(vfs_file) #save new vfs
  324. self.invalidate([os.path.dirname(path)])
  325. emblem, status = self._get_file_status(path)
  326. if emblem is not None:
  327. vfs_file.add_emblem(emblem)
  328. vfs_file.add_string_attribute('hg_status', status)
  329. except StandardError, e:
  330. debugf(e)
  331. return True
  332. if ui.ui().configbool("tortoisehg", "overlayicons", default = True):
  333. class HgExtension(HgExtensionIcons, nautilus.MenuProvider, nautilus.ColumnProvider, nautilus.PropertyPageProvider, nautilus.InfoProvider):
  334. pass
  335. else:
  336. class HgExtension(HgExtensionDefault, nautilus.MenuProvider, nautilus.ColumnProvider, nautilus.PropertyPageProvider):
  337. pass