|
# Trivial Mercurial plugin for Nautilus
#
# Copyright (C) 2007 Steve Borho
#
# Stolen mercilessly from nautilus-bzr, thanks guys
# Copyright (C) 2006 Jeff Bailey
# Copyright (C) 2006 Wouter van Heyst
# Copyright (C) 2006 Jelmer Vernooij
#
# Published under the GNU GPL
import gconf
import gtk
import gobject
from mercurial import hg, ui, match, util
from mercurial.node import short
import nautilus
import os
import subprocess
import sys
import tempfile
import time
import urllib
try:
from mercurial.error import RepoError
except ImportError:
from mercurial.repo import RepoError
TORTOISEHG_PATH = '~/tools/tortoisehg-dev'
TERMINAL_KEY = '/desktop/gnome/applications/terminal/exec'
class HgExtension(nautilus.MenuProvider,
nautilus.ColumnProvider,
nautilus.InfoProvider,
nautilus.PropertyPageProvider):
def __init__(self):
self.cacherepo = None
self.cacheroot = None
self.client = gconf.client_get_default()
thgpath = os.environ.get('TORTOISEHG_PATH',
os.path.expanduser(TORTOISEHG_PATH))
os.environ['TORTOISEHG_PATH'] = thgpath
os.environ['THG_ICON_PATH'] = os.path.join(thgpath, 'icons')
self.hgproc = os.path.join(thgpath, 'hgproc.py')
self.ipath = os.path.join(thgpath, 'icons', 'tortoise')
def icon(self, iname):
return os.path.join(self.ipath, iname)
def get_path_for_vfs_file(self, vfs_file):
if vfs_file.get_uri_scheme() != 'file':
return None
return urllib.unquote(vfs_file.get_uri()[7:])
def clear_cached_repo(self):
self.cacheroot = None
self.cacherepo = None
def get_repo_for_path(self, path):
'''
Find mercurial repository for vfs_file
Returns hg.repo
'''
p = os.path.isdir(path) and path or os.path.dirname(path)
while not os.path.isdir(os.path.join(p, ".hg")):
oldp = p
p = os.path.dirname(p)
if p == oldp:
return None
if p == self.cacheroot:
return self.cacherepo
# Keep one repo cached
try:
self.cacheroot = p
self.cacherepo = hg.repository(ui.ui(), path=p)
return self.cacherepo
except RepoError:
self.cacheroot = None
self.cacherepo = None
return None
def _open_terminal_cb(self, window, vfs_file):
path = self.get_path_for_vfs_file(vfs_file)
if path is None:
return
os.chdir(path)
terminal = self.client.get_string(TERMINAL_KEY)
os.system('%s &' % terminal)
def _about_cb(self, window, vfs_file):
self._run_dialog('about', [vfs_file])
def _add_cb(self, window, vfs_files):
self._run_dialog('add', vfs_files)
self.clear_cached_repo()
def _clone_cb(self, window, vfs_file):
self._run_dialog('clone', [vfs_file])
def _commit_cb(self, window, vfs_files):
self._run_dialog('commit', vfs_files)
self.clear_cached_repo()
def _datamine_cb(self, window, vfs_files):
self._run_dialog('datamine', vfs_files)
def _diff_cb(self, window, vfs_files):
path = self.get_path_for_vfs_file(vfs_files[0])
if path is None:
return
repo = self.get_repo_for_path(path)
if repo is None:
return
diffcmd = repo.ui.config('tortoisehg', 'vdiff', None)
if diffcmd is None:
self._run_dialog('diff', vfs_files)
else:
cmdline = ['hg', diffcmd]
cwd = os.path.isdir(path) and path or os.path.dirname(path)
paths = [self.get_path_for_vfs_file(f) for f in vfs_files]
subprocess.Popen(cmdline + paths, shell=False, cwd=cwd)
def _history_cb(self, window, vfs_files):
self._run_dialog('history', vfs_files)
self.clear_cached_repo()
def _init_cb(self, window, vfs_file):
self._run_dialog('init', [vfs_file])
def _recovery_cb(self, window, vfs_file):
self._run_dialog('recovery', [vfs_file])
self.clear_cached_repo()
def _revert_cb(self, window, vfs_files):
self._run_dialog('revert', vfs_files)
self.clear_cached_repo()
def _serve_cb(self, window, vfs_file):
self._run_dialog('serve', [vfs_file], filelist=False)
def _status_cb(self, window, vfs_file):
self._run_dialog('status', [vfs_file])
def _sync_cb(self, window, vfs_file):
self._run_dialog('synch', [vfs_file], filelist=False)
self.clear_cached_repo()
def _thgconfig_repo_cb(self, window, vfs_file):
self._run_dialog('config', [vfs_file])
def _thgconfig_user_cb(self, window, vfs_file):
self._run_dialog('config', [vfs_file], filelist=False)
def _unmerge_cb(self, window, vfs_file):
self._run_dialog('checkout', [vfs_file], filelist=False,
extras=['--', '--clean', str(self.rev0)])
self.clear_cached_repo()
def _run_dialog(self, hgcmd, vfs_files, filelist=True, extras=[]):
'''
hgcmd - hgproc subcommand
vfs_files - directory, or list of selected files
filelist - bool for whether to generate file list for hgproc
'''
paths = [self.get_path_for_vfs_file(f) for f in vfs_files]
if paths[0] is None:
return
path = paths[0]
repo = self.get_repo_for_path(path)
cwd = os.path.isdir(path) and path or os.path.dirname(path)
if repo is not None:
root = repo.root
else:
root = cwd
cmdopts = [sys.executable, self.hgproc]
cmdopts += ['--root', root]
cmdopts += ['--cwd', cwd]
cmdopts += ['--command', hgcmd]
if filelist:
# Use temporary file to store file list (avoid shell command
# line limitations)
fd, tmpfile = tempfile.mkstemp(prefix="tortoisehg_filelist_")
os.write(fd, "\n".join(paths))
os.close(fd)
cmdopts += ['--listfile', tmpfile, '--deletelistfile']
cmdopts.extend(extras)
subprocess.Popen(cmdopts, cwd=cwd, shell=False)
# Remove cached repo object, dirstate may change
self.cacherepo = None
self.cacheroot = None
def get_background_items(self, window, vfs_file):
'''Build context menu for current directory'''
mainitem = nautilus.MenuItem('HgNautilus', 'Mercurial', '')
submenu = nautilus.Menu()
mainitem.set_submenu(submenu)
path = self.get_path_for_vfs_file(vfs_file)
if path is None:
return
repo = self.get_repo_for_path(path)
if repo is None:
''' The name given to nautilus.MenuItem decides the
of the menu, which is ordered alpahbetically '''
item = nautilus.MenuItem('HgNautilus::newtree',
'Create New Repository',
'Make directory versioned',
self.icon('menucreaterepos.ico'))
item.connect('activate', self._init_cb, vfs_file)
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::clone',
'Create Clone',
'Create clone here from source',
self.icon('menuclone.ico'))
item.connect('activate', self._clone_cb, vfs_file)
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::99about',
'About TortoiseHg',
'Information about TortoiseHg installation',
self.icon('menuabout.ico'))
item.connect('activate', self._about_cb, vfs_file)
submenu.append_item(item)
return mainitem,
if len(repo.changectx(None).parents()) > 1:
self.rev0 = repo.changectx(None).parents()[0].rev()
item = nautilus.MenuItem('HgNautilus::undomerge',
'Undo Merge',
'Clean checkout of original parent revision',
self.icon('menuunmerge.ico'))
item.connect('activate', self._unmerge_cb, vfs_file)
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::10commit',
'Commit',
'Commit changes',
self.icon('menucommit.ico'))
item.connect('activate', self._commit_cb, [vfs_file])
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::20status',
'Show Status',
'Show Repository Status',
self.icon('menushowchanged.ico'))
item.connect('activate', self._status_cb, vfs_file)
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::30diff',
'Visual Diff',
'Show Changes to Repository',
self.icon('menudiff.ico'))
item.connect('activate', self._diff_cb, [vfs_file])
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::40dag',
'Revision History',
'Show revision DAG',
self.icon('menurevisiongraph.ico'))
item.connect('activate', self._history_cb, [vfs_file])
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::50datamine',
'Data Mining',
'Search revision history',
self.icon('menulog.ico'))
item.connect('activate', self._datamine_cb, [vfs_file])
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::60sync',
'Synchronize',
'Sync with another repository',
self.icon('menusynch.ico'))
item.connect('activate', self._sync_cb, vfs_file)
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::70serve',
'Web Server',
'Start internal web server',
self.icon('proxy.ico'))
item.connect('activate', self._serve_cb, vfs_file)
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::75recover',
'Recovery',
'General repair and recovery of repository',
self.icon('general.ico'))
item.connect('activate', self._recovery_cb, vfs_file)
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::80repoconfig',
'Repository Settings',
'Configure Mercurial settings for this repo',
self.icon('menusettings.ico'))
item.connect('activate', self._thgconfig_repo_cb, vfs_file)
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::85userconfig',
'User-Global Settings',
'Configure global Mercurial settings',
self.icon('menusettings.ico'))
item.connect('activate', self._thgconfig_user_cb, vfs_file)
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::99about',
'About TortoiseHg',
'Information about TortoiseHg installation',
self.icon('menuabout.ico'))
item.connect('activate', self._about_cb, vfs_file)
submenu.append_item(item)
return mainitem,
def get_file_items(self, window, vfs_files):
mainitem = nautilus.MenuItem('HgNautilus', 'Mercurial', '')
submenu = nautilus.Menu()
mainitem.set_submenu(submenu)
'''Build context menu for selected files'''
if not vfs_files:
return None
vfs_file = vfs_files[0]
path = self.get_path_for_vfs_file(vfs_file)
repo = self.get_repo_for_path(path)
if repo is None:
if not vfs_file.is_directory():
return None
# Menu for unrevisioned subdirectory
name = vfs_files[0].get_name()
item = nautilus.MenuItem('HgNautilus::10newtree',
'Make directory versioned',
'Create Repository in %s' % name,
self.icon('menucreaterepos.ico'))
item.connect('activate', self._init_cb, vfs_file)
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::20clone',
'Create clone from source',
'Create Clone in %s' % name,
self.icon('menuclone.ico'))
item.connect('activate', self._clone_cb, vfs_file)
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::99about',
'About TortoiseHg',
'Information about TortoiseHg installation',
self.icon('menuabout.ico'))
item.connect('activate', self._about_cb, vfs_file)
submenu.append_item(item)
return mainitem,
localpaths = []
for vfs_file in vfs_files:
path = self.get_path_for_vfs_file(vfs_file)
if path is None:
continue
localpath = path[len(repo.root)+1:]
localpaths.append(localpath)
if not localpaths:
return
path = localpaths[0]
cwd = os.path.isdir(path) and path or os.path.dirname(path)
matcher = match.exact(repo.root, cwd, localpaths)
changes = repo.dirstate.status(matcher, True, True, True)
(lookup, modified, added, removed, deleted, unknown,
ignored, clean) = changes
# Add menu items based on states list
if unknown:
item = nautilus.MenuItem('HgNautilus::30add',
'Add Files',
'Add unversioned files',
self.icon('menuadd.ico'))
item.connect('activate', self._add_cb, vfs_files)
submenu.append_item(item)
if modified or added or removed or deleted or unknown:
item = nautilus.MenuItem('HgNautilus::40commit',
'Commit Files',
'Commit changes',
self.icon('menucommit.ico'))
item.connect('activate', self._commit_cb, vfs_files)
submenu.append_item(item)
item = nautilus.MenuItem('HgNautilus::50revert',
'Undo Changes',
'Revert changes to files',
self.icon('menurevert.ico'))
item.connect('activate', self._revert_cb, vfs_files)
submenu.append_item(item)
if modified or clean:
item = nautilus.MenuItem('HgNautilus::60log',
'File Changelog',
'Show file revision history',
self.icon('menulog.ico'))
item.connect('activate', self._history_cb, vfs_files)
item = nautilus.MenuItem('HgNautilus::annotate',
'Annotate File',
'Annotate file at current revision',
self.icon('menulog.ico'))
item.connect('activate', self._datamine_cb, vfs_files)
submenu.append_item(item)
if modified:
item = nautilus.MenuItem('HgNautilus::70diff',
'File Diffs',
'Show file changes',
self.icon('menudiff.ico'))
item.connect('activate', self._diff_cb, vfs_files)
submenu.append_item(item)
return mainitem,
def get_columns(self):
return nautilus.Column("HgNautilus::80hg_status",
"hg_status",
"HG Status",
"Version control status"),
def _get_file_status(self, repo, localpath):
emblem = None
status = '?'
# This is not what the API is optimized for, but this appears
# to work efficiently enough
matcher = match.always(repo.root, localpath)
changes = repo.dirstate.status(matcher, True, True, True)
(lookup, modified, added, removed, deleted, unknown,
ignored, clean) = changes
if localpath in clean:
emblem = 'default'
status = 'clean'
elif localpath in modified:
emblem = 'cvs-modified'
status = 'modified'
elif localpath in added:
emblem = 'cvs-aded'
status = 'added'
elif localpath in unknown:
emblem = 'new'
status = 'unrevisioned'
elif localpath in ignored:
status = 'ignored'
elif localpath in deleted:
# Should be hard to reach this state
emblem = 'stockmail-priority-high'
status = 'deleted'
return emblem, status
def update_file_info(self, file):
'''Return emblem and hg status for this file'''
path = self.get_path_for_vfs_file(file)
if path is None or file.is_directory():
return
repo = self.get_repo_for_path(path)
if repo is None:
return
localpath = path[len(repo.root)+1:]
emblem, status = self._get_file_status(repo, localpath)
if emblem is not None:
file.add_emblem(emblem)
file.add_string_attribute('hg_status', status)
# property page borrowed from http://www.gnome.org/~gpoo/hg/nautilus-hg/
def __add_row(self, table, row, label_item, label_value):
label = gtk.Label(label_item)
label.set_use_markup(True)
label.set_alignment(1, 0)
table.attach(label, 0, 1, row, row + 1, gtk.FILL, gtk.FILL, 0, 0)
label.show()
label = gtk.Label(label_value)
label.set_use_markup(True)
label.set_alignment(0, 1)
label.show()
table.attach(label, 1, 2, row, row + 1, gtk.FILL, 0, 0, 0)
def get_property_pages(self, vfs_files):
if len(vfs_files) != 1:
return
file = vfs_files[0]
path = self.get_path_for_vfs_file(file)
if path is None or file.is_directory():
return
repo = self.get_repo_for_path(path)
if repo is None:
return
localpath = path[len(repo.root)+1:]
emblem, status = self._get_file_status(repo, localpath)
# Get the information from Mercurial
ctx = repo.changectx(None).parents()[0]
try:
fctx = ctx.filectx(localpath)
rev = fctx.filelog().linkrev(fctx.filerev())
except:
rev = ctx.rev()
ctx = repo.changectx(rev)
node = short(ctx.node())
date = util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2')
parents = '\n'.join([short(p.node()) for p in ctx.parents()])
description = ctx.description()
user = ctx.user()
user = gobject.markup_escape_text(user)
tags = ', '.join(ctx.tags())
branch = ctx.branch()
self.property_label = gtk.Label('Mercurial')
table = gtk.Table(7, 2, False)
table.set_border_width(5)
table.set_row_spacings(5)
table.set_col_spacings(5)
self.__add_row(table, 0, '<b>Status</b>:', status)
self.__add_row(table, 1, '<b>Last-Commit-Revision</b>:', str(rev))
self.__add_row(table, 2, '<b>Last-Commit-Description</b>:', description)
self.__add_row(table, 3, '<b>Last-Commit-Date</b>:', date)
self.__add_row(table, 4, '<b>Last-Commit-User</b>:', user)
if tags:
self.__add_row(table, 5, '<b>Tags</b>:', tags)
if branch != 'default':
self.__add_row(table, 6, '<b>Branch</b>:', branch)
table.show()
return nautilus.PropertyPage("MercurialPropertyPage::status",
self.property_label, table),
|
Loading...