blob: a3eb311f352104c0a38e757de1159eeb4a6573ca [file] [log] [blame]
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Edgewall Software
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://trac.edgewall.com/license.html.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://trac.edgewall.org/.
import os.path
import sys
from genshi.builder import tag
from trac.admin import IAdminCommandProvider, IAdminPanelProvider
from trac.config import ListOption
from trac.core import *
from trac.perm import IPermissionRequestor
from trac.util import as_bool, is_path_below
from trac.util.compat import any
from trac.util.text import breakable_path, normalize_whitespace, print_table, \
printout
from trac.util.translation import _, ngettext, tag_
from trac.versioncontrol import DbRepositoryProvider, RepositoryManager, \
is_default
from trac.web.chrome import Chrome, add_notice, add_warning
class VersionControlAdmin(Component):
"""trac-admin command provider for version control administration."""
implements(IAdminCommandProvider, IPermissionRequestor)
# IAdminCommandProvider methods
def get_admin_commands(self):
yield ('changeset added', '<repos> <rev> [rev] [...]',
"""Notify trac about changesets added to a repository
This command should be called from a post-commit hook. It will
trigger a cache update and notify components about the addition.
""",
self._complete_repos, self._do_changeset_added)
yield ('changeset modified', '<repos> <rev> [rev] [...]',
"""Notify trac about changesets modified in a repository
This command should be called from a post-revprop hook after
revision properties like the commit message, author or date
have been changed. It will trigger a cache update for the given
revisions and notify components about the change.
""",
self._complete_repos, self._do_changeset_modified)
yield ('repository list', '',
'List source repositories',
None, self._do_list)
yield ('repository resync', '<repos> [rev]',
"""Re-synchronize trac with repositories
When [rev] is specified, only that revision is synchronized.
Otherwise, the complete revision history is synchronized. Note
that this operation can take a long time to complete.
If synchronization gets interrupted, it can be resumed later
using the `sync` command.
To synchronize all repositories, specify "*" as the repository.
""",
self._complete_repos, self._do_resync)
yield ('repository sync', '<repos> [rev]',
"""Resume synchronization of repositories
It works like `resync`, except that it doesn't clear the already
synchronized changesets, so it's a better way to resume an
interrupted `resync`.
See `resync` help for detailed usage.
""",
self._complete_repos, self._do_sync)
def get_reponames(self):
rm = RepositoryManager(self.env)
return [reponame or '(default)' for reponame
in rm.get_all_repositories()]
def _complete_repos(self, args):
if len(args) == 1:
return self.get_reponames()
def _do_changeset_added(self, reponame, *revs):
if is_default(reponame):
reponame = ''
rm = RepositoryManager(self.env)
rm.notify('changeset_added', reponame, revs)
def _do_changeset_modified(self, reponame, *revs):
if is_default(reponame):
reponame = ''
rm = RepositoryManager(self.env)
rm.notify('changeset_modified', reponame, revs)
def _do_list(self):
rm = RepositoryManager(self.env)
values = []
for (reponame, info) in sorted(rm.get_all_repositories().iteritems()):
alias = ''
if 'alias' in info:
alias = info['alias'] or '(default)'
values.append((reponame or '(default)', info.get('type', ''),
alias, info.get('dir', '')))
print_table(values, [_('Name'), _('Type'), _('Alias'), _('Directory')])
def _sync(self, reponame, rev, clean):
rm = RepositoryManager(self.env)
if reponame == '*':
if rev is not None:
raise TracError(_('Cannot synchronize a single revision '
'on multiple repositories'))
repositories = rm.get_real_repositories()
else:
if is_default(reponame):
reponame = ''
repos = rm.get_repository(reponame)
if repos is None:
raise TracError(_("Repository '%(repo)s' not found",
repo=reponame or '(default)'))
if rev is not None:
repos.sync_changeset(rev)
printout(_('%(rev)s resynced on %(reponame)s.', rev=rev,
reponame=repos.reponame or '(default)'))
return
repositories = [repos]
for repos in sorted(repositories, key=lambda r: r.reponame):
printout(_('Resyncing repository history for %(reponame)s... ',
reponame=repos.reponame or '(default)'))
repos.sync(self._sync_feedback, clean=clean)
for cnt, in self.env.db_query(
"SELECT count(rev) FROM revision WHERE repos=%s",
(repos.id,)):
printout(ngettext('%(num)s revision cached.',
'%(num)s revisions cached.', num=cnt))
printout(_('Done.'))
def _sync_feedback(self, rev):
sys.stdout.write(' [%s]\r' % rev)
sys.stdout.flush()
def _do_resync(self, reponame, rev=None):
self._sync(reponame, rev, clean=True)
def _do_sync(self, reponame, rev=None):
self._sync(reponame, rev, clean=False)
# IPermissionRequestor methods
def get_permission_actions(self):
return [('VERSIONCONTROL_ADMIN', ['BROWSER_VIEW', 'CHANGESET_VIEW',
'FILE_VIEW', 'LOG_VIEW'])]
class RepositoryAdminPanel(Component):
"""Web admin panel for repository administration."""
implements(IAdminPanelProvider)
allowed_repository_dir_prefixes = ListOption('versioncontrol',
'allowed_repository_dir_prefixes', '',
doc="""Comma-separated list of allowed prefixes for repository
directories when adding and editing repositories in the repository
admin panel. If the list is empty, all repository directories are
allowed. (''since 0.12.1'')""")
# IAdminPanelProvider methods
def get_admin_panels(self, req):
if 'VERSIONCONTROL_ADMIN' in req.perm('admin', 'versioncontrol/repository'):
yield ('versioncontrol', _('Version Control'), 'repository',
_('Repositories'))
def render_admin_panel(self, req, category, page, path_info):
# Retrieve info for all repositories
rm = RepositoryManager(self.env)
all_repos = rm.get_all_repositories()
db_provider = self.env[DbRepositoryProvider]
if path_info:
# Detail view
reponame = path_info if not is_default(path_info) else ''
info = all_repos.get(reponame)
if info is None:
raise TracError(_("Repository '%(repo)s' not found",
repo=path_info))
if req.method == 'POST':
if req.args.get('cancel'):
req.redirect(req.href.admin(category, page))
elif db_provider and req.args.get('save'):
# Modify repository
changes = {}
valid = True
for field in db_provider.repository_attrs:
value = normalize_whitespace(req.args.get(field))
if (value is not None or field == 'hidden') \
and value != info.get(field):
changes[field] = value
if 'dir' in changes and not \
self._check_dir(req, changes['dir']):
valid = False
if valid and changes:
db_provider.modify_repository(reponame, changes)
add_notice(req, _('Your changes have been saved.'))
name = req.args.get('name')
resync = tag.tt('trac-admin $ENV repository resync '
'"%s"' % (name or '(default)'))
if 'dir' in changes:
msg = tag_('You should now run %(resync)s to '
'synchronize Trac with the repository.',
resync=resync)
add_notice(req, msg)
elif 'type' in changes:
msg = tag_('You may have to run %(resync)s to '
'synchronize Trac with the repository.',
resync=resync)
add_notice(req, msg)
if name and name != path_info and not 'alias' in info:
cset_added = tag.tt('trac-admin $ENV changeset '
'added "%s" $REV'
% (name or '(default)'))
msg = tag_('You will need to update your '
'post-commit hook to call '
'%(cset_added)s with the new '
'repository name.',
cset_added=cset_added)
add_notice(req, msg)
if valid:
req.redirect(req.href.admin(category, page))
Chrome(self.env).add_wiki_toolbars(req)
data = {'view': 'detail', 'reponame': reponame}
else:
# List view
if req.method == 'POST':
# Add a repository
if db_provider and req.args.get('add_repos'):
name = req.args.get('name')
type_ = req.args.get('type')
# Avoid errors when copy/pasting paths
dir = normalize_whitespace(req.args.get('dir', ''))
if name is None or type_ is None or not dir:
add_warning(req, _('Missing arguments to add a '
'repository.'))
elif self._check_dir(req, dir):
try:
db_provider.add_repository(name, dir, type_)
except self.env.db_exc.IntegrityError:
name = name or '(default)'
raise TracError(_('The repository "%(name)s" '
'already exists.', name=name))
name = name or '(default)'
add_notice(req, _('The repository "%(name)s" has been '
'added.', name=name))
resync = tag.tt('trac-admin $ENV repository resync '
'"%s"' % name)
msg = tag_('You should now run %(resync)s to '
'synchronize Trac with the repository.',
resync=resync)
add_notice(req, msg)
cset_added = tag.tt('trac-admin $ENV changeset '
'added "%s" $REV' % name)
msg = tag_('You should also set up a post-commit hook '
'on the repository to call %(cset_added)s '
'for each committed changeset.',
cset_added=cset_added)
add_notice(req, msg)
req.redirect(req.href.admin(category, page))
# Add a repository alias
elif db_provider and req.args.get('add_alias'):
name = req.args.get('name')
alias = req.args.get('alias')
if name is not None and alias is not None:
try:
db_provider.add_alias(name, alias)
except self.env.db_exc.IntegrityError:
raise TracError(_('The alias "%(name)s" already '
'exists.',
name=name or '(default)'))
add_notice(req, _('The alias "%(name)s" has been '
'added.', name=name or '(default)'))
req.redirect(req.href.admin(category, page))
add_warning(req, _('Missing arguments to add an '
'alias.'))
# Refresh the list of repositories
elif req.args.get('refresh'):
req.redirect(req.href.admin(category, page))
# Remove repositories
elif db_provider and req.args.get('remove'):
sel = req.args.getlist('sel')
if sel:
for name in sel:
db_provider.remove_repository(name)
add_notice(req, _('The selected repositories have '
'been removed.'))
req.redirect(req.href.admin(category, page))
add_warning(req, _('No repositories were selected.'))
data = {'view': 'list'}
# Find repositories that are editable
db_repos = {}
if db_provider is not None:
db_repos = dict(db_provider.get_repositories())
# Prepare common rendering data
repositories = dict((reponame, self._extend_info(reponame, info.copy(),
reponame in db_repos))
for (reponame, info) in all_repos.iteritems())
types = sorted([''] + rm.get_supported_types())
data.update({'types': types, 'default_type': rm.repository_type,
'repositories': repositories})
return 'admin_repositories.html', data
def _extend_info(self, reponame, info, editable):
"""Extend repository info for rendering."""
info['name'] = reponame
if info.get('dir') is not None:
info['prettydir'] = breakable_path(info['dir']) or ''
info['hidden'] = as_bool(info.get('hidden'))
info['editable'] = editable
if not info.get('alias'):
try:
repos = RepositoryManager(self.env).get_repository(reponame)
youngest_rev = repos.get_youngest_rev()
info['rev'] = youngest_rev
info['display_rev'] = repos.display_rev(youngest_rev)
except Exception:
pass
return info
def _check_dir(self, req, dir):
"""Check that a repository directory is valid, and add a warning
message if not.
"""
if not os.path.isabs(dir):
add_warning(req, _('The repository directory must be an absolute '
'path.'))
return False
prefixes = [os.path.join(self.env.path, prefix)
for prefix in self.allowed_repository_dir_prefixes]
if prefixes and not any(is_path_below(dir, prefix)
for prefix in prefixes):
add_warning(req, _('The repository directory must be located '
'below one of the following directories: '
'%(dirs)s', dirs=', '.join(prefixes)))
return False
return True