# -*- coding: utf-8 -*-
"""
rhodecode.controllers.admin.repos
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Admin controller for RhodeCode
:created_on: Apr 7, 2010
:author: marcink
:copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
:license: GPLv3, see COPYING for more details.
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; version 2
# of the License or (at your opinion) any later version of the license.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
import logging
import traceback
import formencode
from operator import itemgetter
from formencode import htmlfill
from paste.httpexceptions import HTTPInternalServerError
from pylons import request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect
from pylons.i18n.translation import _
from rhodecode.lib import helpers as h
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
HasPermissionAnyDecorator
from rhodecode.lib.base import BaseController, render
from rhodecode.lib.utils import invalidate_cache, action_logger, repo_name_slug
from rhodecode.lib.helpers import get_token
from rhodecode.model.db import User, Repository, UserFollowing
from rhodecode.model.db import User, Repository, UserFollowing, Group
from rhodecode.model.forms import RepoForm
from rhodecode.model.scm import ScmModel
from rhodecode.model.repo import RepoModel
log = logging.getLogger(__name__)
class ReposController(BaseController):
REST Controller styled on the Atom Publishing Protocol"""
# To properly map this controller, ensure your config/routing.py
# file has a resource setup:
# map.resource('repo', 'repos')
@LoginRequired()
@HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
def __before__(self):
c.admin_user = session.get('admin_user')
c.admin_username = session.get('admin_username')
super(ReposController, self).__before__()
def __load_data(self, repo_name):
Load defaults settings for edit, and update
:param repo_name:
repo, dbrepo = ScmModel().get(repo_name, retval='repo')
repo_model = RepoModel()
c.repo_info = repo_model.get_by_repo_name(repo_name)
if c.repo_info is None:
h.flash(_('%s repository is not mapped to db perhaps'
' it was created or renamed from the filesystem'
' please run the application again'
' in order to rescan repositories') % repo_name,
category='error')
return redirect(url('repos'))
c.repo_groups = [('', '')]
c.repo_groups.extend([(x.group_id, x.group_name) for x in self.sa.query(Group).all()])
c.default_user_id = User.by_username('default').user_id
c.in_public_journal = self.sa.query(UserFollowing)\
.filter(UserFollowing.user_id == c.default_user_id)\
.filter(UserFollowing.follows_repository == c.repo_info).scalar()
if c.repo_info.stats:
last_rev = c.repo_info.stats.stat_on_revision
else:
last_rev = 0
c.stats_revision = last_rev
c.repo_last_rev = repo.count() - 1 if repo.revisions else 0
if last_rev == 0 or c.repo_last_rev == 0:
c.stats_percentage = 0
c.stats_percentage = '%.2f' % ((float((last_rev)) /
c.repo_last_rev) * 100)
c.users_array = repo_model.get_users_js()
c.users_groups_array = repo_model.get_users_groups_js()
defaults = c.repo_info.get_dict()
group, repo_name = c.repo_info.groups_and_repo
defaults['repo_name'] = repo_name
defaults['repo_group'] = getattr(group, 'group_id', None)
#fill owner
if c.repo_info.user:
defaults.update({'user':c.repo_info.user.username})
replacement_user = self.sa.query(User)\
.filter(User.admin == True).first().username
defaults.update({'user':replacement_user})
#fill repository users
for p in c.repo_info.repo_to_perm:
defaults.update({'u_perm_%s' % p.user.username:
p.permission.permission_name})
#fill repository groups
for p in c.repo_info.users_group_to_perm:
defaults.update({'g_perm_%s' % p.users_group.users_group_name:
return defaults
@HasPermissionAllDecorator('hg.admin')
def index(self, format='html'):
"""GET /repos: All items in the collection"""
# url('repos')
cached_repo_list = ScmModel().get_repos()
c.repos_list = sorted(cached_repo_list, key=itemgetter('name_sort'))
return render('admin/repos/repos.html')
def create(self):
POST /repos: Create a new item"""
_form = RepoForm()()
form_result = {}
try:
form_result = _form.to_python(dict(request.POST))
form_result = RepoForm()(repo_groups=c.repo_groups).to_python(dict(request.POST))
repo_model.create(form_result, c.rhodecode_user)
h.flash(_('created repository %s') % form_result['repo_name'],
if form_result['clone_uri']:
h.flash(_('created repository %s from %s') \
% (form_result['repo_name'], form_result['clone_uri']),
category='success')
if request.POST.get('user_created'):
action_logger(self.rhodecode_user, 'user_created_repo',
form_result['repo_name'], '', self.sa)
action_logger(self.rhodecode_user, 'admin_created_repo',
except formencode.Invalid, errors:
c.new_repo = errors.value['repo_name']
r = render('admin/repos/repo_add_create_repository.html')
r = render('admin/repos/repo_add.html')
return htmlfill.render(
r,
defaults=errors.value,
errors=errors.error_dict or {},
prefix_error=False,
encoding="UTF-8")
except Exception:
log.error(traceback.format_exc())
msg = _('error occurred during creation of repository %s') \
% form_result.get('repo_name')
h.flash(msg, category='error')
return redirect(url('home'))
def new(self, format='html'):
"""GET /repos/new: Form to create a new item"""
new_repo = request.GET.get('repo', '')
c.new_repo = repo_name_slug(new_repo)
return render('admin/repos/repo_add.html')
def update(self, repo_name):
PUT /repos/repo_name: Update an existing item"""
# Forms posted to this method should contain a hidden field:
# <input type="hidden" name="_method" value="PUT" />
# Or using helpers:
# h.form(url('repo', repo_name=ID),
# method='put')
# url('repo', repo_name=ID)
changed_name = repo_name
_form = RepoForm(edit=True, old_data={'repo_name':repo_name})()
repo_model.update(repo_name, form_result)
invalidate_cache('get_repo_cached_%s' % repo_name)
h.flash(_('Repository %s updated successfully' % repo_name),
changed_name = form_result['repo_name']
action_logger(self.rhodecode_user, 'admin_updated_repo',
changed_name, '', self.sa)
if last_rev == 0:
errors.value.update({'user':c.repo_info.user.username})
defaults = self.__load_data(repo_name)
defaults.update(errors.value)
render('admin/repos/repo_edit.html'),
defaults=defaults,
h.flash(_('error occurred during update of repository %s') \
% repo_name, category='error')
return redirect(url('edit_repo', repo_name=changed_name))
def delete(self, repo_name):
DELETE /repos/repo_name: Delete an existing item"""
# <input type="hidden" name="_method" value="DELETE" />
# method='delete')
repo = repo_model.get_by_repo_name(repo_name)
if not repo:
' it was moved or renamed from the filesystem'
action_logger(self.rhodecode_user, 'admin_deleted_repo',
repo_name, '', self.sa)
repo_model.delete(repo)
h.flash(_('deleted repository %s') % repo_name, category='success')
except Exception, e:
h.flash(_('An error occurred during deletion of %s') % repo_name,
def delete_perm_user(self, repo_name):
DELETE an existing repository permission user
repo_model.delete_perm_user(request.POST, repo_name)
@@ -280,111 +340,56 @@ class ReposController(BaseController):
ScmModel().mark_for_invalidation(repo_name)
h.flash(_('An error occurred during cache invalidation'),
return redirect(url('edit_repo', repo_name=repo_name))
def repo_public_journal(self, repo_name):
Set's this repository to be visible in public journal,
in other words assing default user to follow this repo
cur_token = request.POST.get('auth_token')
token = get_token()
if cur_token == token:
repo_id = Repository.by_repo_name(repo_name).repo_id
user_id = User.by_username('default').user_id
self.scm_model.toggle_following_repo(repo_id, user_id)
h.flash(_('Updated repository visibility in public journal'),
except:
h.flash(_('An error occurred during setting this'
' repository in public journal'),
h.flash(_('Token mismatch'), category='error')
def show(self, repo_name, format='html'):
"""GET /repos/repo_name: Show a specific item"""
def edit(self, repo_name, format='html'):
"""GET /repos/repo_name/edit: Form to edit an existing item"""
# url('edit_repo', repo_name=ID)
encoding="UTF-8",
force_defaults=False
)
rhodecode.controllers.admin.settings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
settings controller for rhodecode admin
:created_on: Jul 14, 2010
from sqlalchemy import func
from pylons import request, session, tmpl_context as c, url, config
HasPermissionAnyDecorator, NotAnonymous
from rhodecode.lib.celerylib import tasks, run_task
from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \
set_rhodecode_config, repo_name_slug
from rhodecode.model.db import RhodeCodeUi, Repository
from rhodecode.model.db import RhodeCodeUi, Repository, Group
from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \
ApplicationUiSettingsForm
from rhodecode.model.settings import SettingsModel
from rhodecode.model.user import UserModel
class SettingsController(BaseController):
"""REST Controller styled on the Atom Publishing Protocol"""
# map.resource('setting', 'settings', controller='admin/settings',
# path_prefix='/admin', name_prefix='admin_')
super(SettingsController, self).__before__()
"""GET /admin/settings: All items in the collection"""
# url('admin_settings')
defaults = SettingsModel().get_app_settings()
defaults.update(self.get_hg_ui_settings())
render('admin/settings/settings.html'),
"""POST /admin/settings: Create a new item"""
"""GET /admin/settings/new: Form to create a new item"""
# url('admin_new_setting')
@@ -276,72 +276,73 @@ class SettingsController(BaseController)
def my_account_update(self):
"""PUT /_admin/my_account_update: Update an existing item"""
# h.form(url('admin_settings_my_account_update'),
# url('admin_settings_my_account_update', id=ID)
user_model = UserModel()
uid = c.rhodecode_user.user_id
_form = UserForm(edit=True, old_data={'user_id':uid,
'email':c.rhodecode_user.email})()
user_model.update_my_account(uid, form_result)
h.flash(_('Your account was updated successfully'),
c.user = user_model.get(c.rhodecode_user.user_id, cache=False)
c.user = UserModel().get(c.rhodecode_user.user_id, cache=False)
all_repos = self.sa.query(Repository)\
.filter(Repository.user_id == c.user.user_id)\
.order_by(func.lower(Repository.repo_name))\
.all()
c.user_repos = ScmModel().get_repos(all_repos)
render('admin/users/user_edit_my_account.html'),
h.flash(_('error occurred during update of user %s') \
% form_result.get('username'), category='error')
return redirect(url('my_account'))
@NotAnonymous()
def create_repository(self):
"""GET /_admin/create_repository: Form to create a new item"""
return render('admin/repos/repo_add_create_repository.html')
def get_hg_ui_settings(self):
ret = self.sa.query(RhodeCodeUi).all()
if not ret:
raise Exception('Could not get application ui settings !')
settings = {}
for each in ret:
k = each.ui_key
v = each.ui_value
if k == '/':
k = 'root_path'
if k.find('.') != -1:
k = k.replace('.', '_')
if each.ui_section == 'hooks':
v = each.ui_active
settings[each.ui_section + '_' + k] = v
return settings
@@ -405,107 +405,111 @@ def UserForm(edit=False, old_data={}):
email = All(Email(not_empty=True), UniqSystemEmail(old_data))
chained_validators = [ValidPassword]
return _UserForm
def UsersGroupForm(edit=False, old_data={}, available_members=[]):
class _UsersGroupForm(formencode.Schema):
allow_extra_fields = True
filter_extra_fields = True
users_group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
ValidUsersGroup(edit, old_data))
users_group_active = StringBoolean(if_missing=False)
if edit:
users_group_members = OneOf(available_members, hideList=False,
testValueList=True,
if_missing=None, not_empty=False)
return _UsersGroupForm
def RegisterForm(edit=False, old_data={}):
class _RegisterForm(formencode.Schema):
username = All(ValidUsername(edit, old_data),
UnicodeString(strip=True, min=1, not_empty=True))
password = All(UnicodeString(strip=True, min=6, not_empty=True))
password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=True))
active = StringBoolean(if_missing=False)
name = UnicodeString(strip=True, min=1, not_empty=True)
lastname = UnicodeString(strip=True, min=1, not_empty=True)
chained_validators = [ValidPasswordsMatch, ValidPassword]
return _RegisterForm
def PasswordResetForm():
class _PasswordResetForm(formencode.Schema):
email = All(ValidSystemEmail(), Email(not_empty=True))
return _PasswordResetForm
def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
repo_groups=[]):
class _RepoForm(formencode.Schema):
filter_extra_fields = False
repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
ValidRepoName(edit, old_data))
clone_uri = UnicodeString(strip=True, min=1, not_empty=False)
repo_group = OneOf(repo_groups)
repo_type = OneOf(supported_backends)
description = UnicodeString(strip=True, min=1, not_empty=True)
private = StringBoolean(if_missing=False)
enable_statistics = StringBoolean(if_missing=False)
enable_downloads = StringBoolean(if_missing=False)
#this is repo owner
user = All(Int(not_empty=True), ValidRepoUser)
chained_validators = [ValidPerms]
return _RepoForm
def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
class _RepoForkForm(formencode.Schema):
fork_name = All(UnicodeString(strip=True, min=1, not_empty=True),
repo_type = All(ValidForkType(old_data), OneOf(supported_backends))
return _RepoForkForm
def RepoSettingsForm(edit=False, old_data={}):
chained_validators = [ValidPerms, ValidSettings]
def ApplicationSettingsForm():
class _ApplicationSettingsForm(formencode.Schema):
rhodecode_title = UnicodeString(strip=True, min=1, not_empty=True)
rhodecode_realm = UnicodeString(strip=True, min=1, not_empty=True)
rhodecode_ga_code = UnicodeString(strip=True, min=1, not_empty=False)
return _ApplicationSettingsForm
def ApplicationUiSettingsForm():
class _ApplicationUiSettingsForm(formencode.Schema):
web_push_ssl = OneOf(['true', 'false'], if_missing='false')
paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=1, not_empty=True))
hooks_changegroup_update = OneOf(['True', 'False'], if_missing=False)
@@ -189,159 +189,161 @@ class RepoModel(BaseModel):
self.sa.rollback()
raise
def create(self, form_data, cur_user, just_db=False, fork=False):
if fork:
#force str since hg doesn't go with unicode
repo_name = str(form_data['fork_name'])
org_name = str(form_data['repo_name'])
org_name = repo_name = str(form_data['repo_name'])
new_repo = Repository()
new_repo.enable_statistics = False
for k, v in form_data.items():
if k == 'repo_name':
v = repo_name
setattr(new_repo, k, v)
parent_repo = self.sa.query(Repository)\
.filter(Repository.repo_name == org_name).scalar()
new_repo.fork = parent_repo
new_repo.user_id = cur_user.user_id
self.sa.add(new_repo)
#create default permission
repo_to_perm = RepoToPerm()
default = 'repository.read'
for p in UserModel(self.sa).get_by_username('default', cache=False).user_perms:
if p.permission.permission_name.startswith('repository.'):
default = p.permission.permission_name
break
default_perm = 'repository.none' if form_data['private'] else default
repo_to_perm.permission_id = self.sa.query(Permission)\
.filter(Permission.permission_name == default_perm)\
.one().permission_id
repo_to_perm.repository = new_repo
repo_to_perm.user_id = UserModel(self.sa)\
.get_by_username('default', cache=False).user_id
self.sa.add(repo_to_perm)
if not just_db:
self.__create_repo(repo_name, form_data['repo_type'])
self.__create_repo(repo_name, form_data['repo_type'],
form_data['clone_uri'])
self.sa.commit()
#now automatically start following this repository as owner
ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
cur_user.user_id)
def create_fork(self, form_data, cur_user):
run_task(tasks.create_repo_fork, form_data, cur_user)
def delete(self, repo):
self.sa.delete(repo)
self.__delete_repo(repo)
def delete_perm_user(self, form_data, repo_name):
self.sa.query(RepoToPerm)\
.filter(RepoToPerm.repository \
== self.get_by_repo_name(repo_name))\
.filter(RepoToPerm.user_id == form_data['user_id']).delete()
def delete_perm_users_group(self, form_data, repo_name):
self.sa.query(UsersGroupToPerm)\
.filter(UsersGroupToPerm.repository \
.filter(UsersGroupToPerm.users_group_id \
== form_data['users_group_id']).delete()
def delete_stats(self, repo_name):
self.sa.query(Statistics)\
.filter(Statistics.repository == \
self.get_by_repo_name(repo_name)).delete()
def __create_repo(self, repo_name, alias):
def __create_repo(self, repo_name, alias, clone_uri=False):
makes repository on filesystem
:param alias:
from rhodecode.lib.utils import check_repo
repo_path = os.path.join(self.repos_path, repo_name)
if check_repo(repo_name, self.repos_path):
log.info('creating repo %s in %s', repo_name, repo_path)
log.info('creating repo %s in %s @ %s', repo_name, repo_path,
clone_uri)
backend = get_backend(alias)
backend(repo_path, create=True)
backend(repo_path, create=True, src_url=clone_uri)
def __rename_repo(self, old, new):
renames repository on filesystem
:param old: old name
:param new: new name
log.info('renaming repo from %s to %s', old, new)
old_path = os.path.join(self.repos_path, old)
new_path = os.path.join(self.repos_path, new)
if os.path.isdir(new_path):
raise Exception('Was trying to rename to already existing dir %s',
new_path)
shutil.move(old_path, new_path)
def __delete_repo(self, repo):
removes repo from filesystem, the removal is acctually made by
added rm__ prefix into dir, and rename internat .hg/.git dirs so this
repository is no longer valid for rhodecode, can be undeleted later on
by reverting the renames on this repository
:param repo: repo object
rm_path = os.path.join(self.repos_path, repo.repo_name)
log.info("Removing %s", rm_path)
#disable hg/git
alias = repo.repo_type
shutil.move(os.path.join(rm_path, '.%s' % alias),
os.path.join(rm_path, 'rm__.%s' % alias))
#disable repo
shutil.move(rm_path, os.path.join(self.repos_path, 'rm__%s__%s' \
% (datetime.today().isoformat(),
repo.repo_name)))
## -*- coding: utf-8 -*-
${h.form(url('repos'))}
<div class="form">
<!-- fields -->
<div class="fields">
<div class="field">
<div class="label">
<label for="repo_name">${_('Name')}:</label>
</div>
<div class="input">
${h.text('repo_name',c.new_repo,class_="small")}
<label for="repo_name">${_('Clone from')}:</label>
<label for="clone_uri">${_('Clone from')}:</label>
${h.text('clone_uri',c.new_repo,class_="small")}
${h.text('clone_uri',class_="small")}
<label for="repo_group">${_('Repository group')}:</label>
${h.select('repo_group','',c.repo_groups,class_="medium")}
<span>${h.link_to(_('add new group'),h.url(''))}</span>
<label for="repo_type">${_('Type')}:</label>
${h.select('repo_type','hg',c.backends,class_="small")}
<div class="label label-textarea">
<label for="description">${_('Description')}:</label>
<div class="textarea text-area editor">
${h.textarea('description',cols=23,rows=5)}
<div class="label label-checkbox">
<label for="private">${_('Private')}:</label>
<div class="checkboxes">
${h.checkbox('private',value="True")}
<div class="buttons">
${h.submit('add','add',class_="ui-button")}
${h.end_form()}
<%inherit file="/base/base.html"/>
<%def name="title()">
${_('Edit repository')} ${c.repo_info.repo_name} - ${c.rhodecode_name}
</%def>
<%def name="breadcrumbs_links()">
${h.link_to(_('Admin'),h.url('admin_home'))}
»
${h.link_to(_('Repositories'),h.url('repos'))}
${_('edit')} » ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
<%def name="page_nav()">
${self.menu('admin')}
<%def name="main()">
<div class="box box-left">
<!-- box / title -->
<div class="title">
${self.breadcrumbs()}
${h.form(url('repo', repo_name=c.repo_info.repo_name),method='put')}
${h.text('repo_name',class_="medium")}
<label for="clone_uri">${_('Clone uri')}:</label>
${h.select('repo_type','hg',c.backends,class_="medium")}
<label for="enable_statistics">${_('Enable statistics')}:</label>
${h.checkbox('enable_statistics',value="True")}
<label for="enable_downloads">${_('Enable downloads')}:</label>
${h.checkbox('enable_downloads',value="True")}
<label for="user">${_('Owner')}:</label>
<div class="input input-small ac">
<div class="perm_ac">
${h.text('user',class_='yui-ac-input')}
<div id="owner_container"></div>
<label for="input">${_('Permissions')}:</label>
@@ -20,97 +20,107 @@
<!-- end box / title -->
<label>${_('Name')}:</label>
<div class="input-short">
%if c.dbrepo.repo_type =='hg':
<img style="margin-bottom:2px" class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url("/images/icons/hgicon.png")}"/>
%endif
%if c.dbrepo.repo_type =='git':
<img style="margin-bottom:2px" class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url("/images/icons/giticon.png")}"/>
%if c.dbrepo.private:
<img style="margin-bottom:2px" class="icon" title="${_('private repository')}" alt="${_('private repository')}" src="${h.url("/images/icons/lock.png")}"/>
%else:
<img style="margin-bottom:2px" class="icon" title="${_('public repository')}" alt="${_('public repository')}" src="${h.url("/images/icons/lock_open.png")}"/>
<span style="font-size: 1.6em;font-weight: bold;vertical-align: baseline;">${c.repo.name}</span>
%if c.rhodecode_user.username != 'default':
%if c.following:
<span id="follow_toggle" class="following" title="${_('Stop following this repository')}"
onclick="javascript:toggleFollowingRepo(this,${c.dbrepo.repo_id},'${str(h.get_token())}')">
</span>
<span id="follow_toggle" class="follow" title="${_('Start following this repository')}"
%endif:
<br/>
%if c.dbrepo.fork:
<span style="margin-top:5px">
<a href="${h.url('summary_home',repo_name=c.dbrepo.fork.repo_name)}">
<img class="icon" alt="${_('public')}"
title="${_('Fork of')} ${c.dbrepo.fork.repo_name}"
src="${h.url("/images/icons/arrow_divide.png")}"/>
${_('Fork of')} ${c.dbrepo.fork.repo_name}
</a>
%if c.dbrepo.clone_uri:
<a href="${h.url(str(c.dbrepo.clone_uri))}">
<img class="icon" alt="${_('remote clone')}"
title="${_('Clone from')} ${c.dbrepo.clone_uri}"
src="${h.url("/images/icons/connect.png")}"/>
${_('Clone from')} ${c.dbrepo.clone_uri}
<label>${_('Description')}:</label>
${c.dbrepo.description}
<label>${_('Contact')}:</label>
<div class="gravatar">
<img alt="gravatar" src="${h.gravatar_url(c.dbrepo.user.email)}"/>
${_('Username')}: ${c.dbrepo.user.username}<br/>
${_('Name')}: ${c.dbrepo.user.name} ${c.dbrepo.user.lastname}<br/>
${_('Email')}: <a href="mailto:${c.dbrepo.user.email}">${c.dbrepo.user.email}</a>
<label>${_('Last change')}:</label>
${h.age(c.repo.last_change)} - ${c.repo.last_change}
${_('by')} ${h.get_changeset_safe(c.repo,'tip').author}
<label>${_('Clone url')}:</label>
<input type="text" id="clone_url" readonly="readonly" value="hg clone ${c.clone_repo_url}" size="70"/>
Status change: