PageRenderTime 42ms CodeModel.GetById 16ms app.highlight 21ms RepoModel.GetById 2ms app.codeStats 0ms

/contrib/nautilus-thg.py

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