@@ -465,19 +465,20 @@ def make_map(config):
conditions=dict(function=check_repo))
rmap.connect('repo_fork_create_home', '/{repo_name:.*}/fork',
controller='settings', action='fork_create',
controller='forks', action='fork_create',
conditions=dict(function=check_repo, method=["POST"]))
rmap.connect('repo_fork_home', '/{repo_name:.*}/fork',
controller='settings', action='fork',
controller='forks', action='fork',
rmap.connect('repo_forks_home', '/{repo_name:.*}/forks',
controller='forks', action='forks',
rmap.connect('repo_followers_home', '/{repo_name:.*}/followers',
controller='followers', action='followers',
return rmap
@@ -29,9 +29,10 @@ import formencode
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 import request, session, tmpl_context as c, url
from pylons.controllers.util import redirect
from pylons.i18n.translation import _
from sqlalchemy.exc import IntegrityError
from rhodecode.lib import helpers as h
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
@@ -39,11 +40,11 @@ from rhodecode.lib.auth import LoginRequ
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.meta import Session
from rhodecode.model.db import User, Repository, UserFollowing, RepoGroup
from rhodecode.model.forms import RepoForm
from rhodecode.model.scm import ScmModel
from rhodecode.model.repo import RepoModel
log = logging.getLogger(__name__)
@@ -65,7 +66,7 @@ class ReposController(BaseController):
def __load_defaults(self):
c.repo_groups = RepoGroup.groups_choices()
c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
repo_model = RepoModel()
c.users_array = repo_model.get_users_js()
c.users_groups_array = repo_model.get_users_groups_js()
@@ -127,13 +128,13 @@ class ReposController(BaseController):
"""
POST /repos: Create a new item"""
# url('repos')
self.__load_defaults()
form_result = {}
try:
form_result = RepoForm(repo_groups=c.repo_groups_choices)()\
.to_python(dict(request.POST))
repo_model.create(form_result, self.rhodecode_user)
RepoModel().create(form_result, self.rhodecode_user)
if form_result['clone_uri']:
h.flash(_('created repository %s from %s') \
% (form_result['repo_name'], form_result['clone_uri']),
@@ -143,13 +144,13 @@ class ReposController(BaseController):
category='success')
if request.POST.get('user_created'):
#created by regular non admin user
# created by regular non admin user
action_logger(self.rhodecode_user, 'user_created_repo',
form_result['repo_name_full'], '', self.sa)
else:
action_logger(self.rhodecode_user, 'admin_created_repo',
Session().commit()
except formencode.Invalid, errors:
c.new_repo = errors.value['repo_name']
@@ -207,7 +208,7 @@ class ReposController(BaseController):
changed_name = repo.repo_name
action_logger(self.rhodecode_user, 'admin_updated_repo',
changed_name, '', self.sa)
defaults = self.__load_data(repo_name)
defaults.update(errors.value)
@@ -251,7 +252,7 @@ class ReposController(BaseController):
repo_model.delete(repo)
invalidate_cache('get_repo_cached_%s' % repo_name)
h.flash(_('deleted repository %s') % repo_name, category='success')
except IntegrityError, e:
if e.message.find('repositories_fork_id_fkey'):
log.error(traceback.format_exc())
@@ -23,13 +23,23 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import formencode
import traceback
from pylons import tmpl_context as c, request
from pylons import tmpl_context as c, request, url
import rhodecode.lib.helpers as h
from rhodecode.lib.helpers import Page
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
NotAnonymous
from rhodecode.lib.base import BaseRepoController, render
from rhodecode.model.db import Repository, User, UserFollowing
from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
from rhodecode.model.forms import RepoForkForm
@@ -37,11 +47,59 @@ log = logging.getLogger(__name__)
class ForksController(BaseRepoController):
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
def __before__(self):
super(ForksController, self).__before__()
def __load_data(self, repo_name=None):
Load defaults settings for edit, and update
:param repo_name:
c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
repo = db_repo.scm_instance
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.default_user_id = User.get_by_username('default').user_id
c.in_public_journal = UserFollowing.query()\
.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
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)
defaults = RepoModel()._get_defaults(repo_name)
# add prefix to fork
defaults['repo_name'] = 'fork-' + defaults['repo_name']
return defaults
def forks(self, repo_name):
p = int(request.params.get('page', 1))
repo_id = c.rhodecode_db_repo.repo_id
@@ -54,3 +112,63 @@ class ForksController(BaseRepoController
return c.forks_data
return render('/forks/forks.html')
@NotAnonymous()
def fork(self, repo_name):
c.repo_info = Repository.get_by_repo_name(repo_name)
if not c.repo_info:
' it was created or renamed from the file system'
return redirect(url('home'))
return htmlfill.render(
render('forks/fork.html'),
defaults=defaults,
encoding="UTF-8",
force_defaults=False
)
def fork_create(self, repo_name):
_form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
repo_groups=c.repo_groups_choices,)()
form_result = _form.to_python(dict(request.POST))
# add org_path of repo so we can do a clone from it later
form_result['org_path'] = c.repo_info.repo_name
# create fork is done sometimes async on celery, db transaction
# management is handled there.
RepoModel().create_fork(form_result, self.rhodecode_user)
h.flash(_('forked %s repository as %s') \
% (repo_name, form_result['repo_name']),
defaults=errors.value,
errors=errors.error_dict or {},
prefix_error=False,
encoding="UTF-8")
except Exception:
h.flash(_('An error occurred during repository forking %s') %
repo_name, category='error')
@@ -23,21 +23,22 @@
from itertools import groupby
from sqlalchemy import or_
from sqlalchemy.orm import joinedload, make_transient
from sqlalchemy.orm import joinedload
from webhelpers.paginate import Page
from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
from paste.httpexceptions import HTTPBadRequest
from pylons import request, tmpl_context as c, response, url
from rhodecode.lib.auth import LoginRequired, NotAnonymous
from rhodecode.model.db import UserLog, UserFollowing
@@ -124,6 +125,7 @@ class JournalController(BaseController):
self.scm_model.toggle_following_user(user_id,
self.rhodecode_user.user_id)
return 'ok'
except:
raise HTTPBadRequest()
@@ -133,6 +135,7 @@ class JournalController(BaseController):
self.scm_model.toggle_following_repo(repo_id,
@@ -35,14 +35,14 @@ from pylons.i18n.translation import _
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAllDecorator, \
HasRepoPermissionAnyDecorator, NotAnonymous
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAllDecorator
from rhodecode.lib.utils import invalidate_cache, action_logger
from rhodecode.model.forms import RepoSettingsForm, RepoForkForm
from rhodecode.model.forms import RepoSettingsForm
from rhodecode.model.db import RepoGroup
@@ -52,15 +52,15 @@ class SettingsController(BaseRepoControl
super(SettingsController, self).__before__()
@HasRepoPermissionAllDecorator('repository.admin')
def index(self, repo_name):
@@ -89,15 +89,15 @@ class SettingsController(BaseRepoControl
def update(self, repo_name):
changed_name = repo_name
_form = RepoSettingsForm(edit=True,
old_data={'repo_name': repo_name},
repo_groups=c.repo_groups_choices)()
repo_model.update(repo_name, form_result)
h.flash(_('Repository %s updated successfully' % repo_name),
@@ -105,6 +105,7 @@ class SettingsController(BaseRepoControl
changed_name = form_result['repo_name_full']
action_logger(self.rhodecode_user, 'user_updated_repo',
c.repo_info = repo_model.get_by_repo_name(repo_name)
@@ -148,61 +149,10 @@ class SettingsController(BaseRepoControl
h.flash(_('An error occurred during deletion of %s') % repo_name,
c.repo_info = repo = repo_model.get_by_repo_name(repo_name)
if not repo:
return render('settings/repo_fork.html')
_form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type})()
form_result.update({'repo_name': repo_name})
repo_model.create_fork(form_result, self.rhodecode_user)
% (repo_name, form_result['fork_name']),
action_logger(self.rhodecode_user,
'user_forked_repo:%s' % form_result['fork_name'],
repo_name, '', self.sa)
c.new_repo = errors.value['fork_name']
r = render('settings/repo_fork.html')
r,
@@ -37,29 +37,28 @@ from string import lower
from pylons import config, url
from vcs import get_backend
from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str
from rhodecode.lib.celerylib import run_task, locked_task, str2bool, \
__get_lockkey, LockHeld, DaemonLock
from rhodecode.lib.helpers import person
from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer
from rhodecode.lib.utils import add_cache
from rhodecode.lib.utils import add_cache, action_logger
from rhodecode.lib.compat import json, OrderedDict
from rhodecode.model import init_model
from rhodecode.model import meta
from rhodecode.model.db import RhodeCodeUi, Statistics, Repository, User
from vcs.backends import get_repo
from rhodecode.model.db import Statistics, Repository, User
from sqlalchemy import engine_from_config
add_cache(config)
__all__ = ['whoosh_index', 'get_commits_stats',
'reset_user_password', 'send_email']
CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
@@ -81,17 +80,13 @@ def get_logger(cls):
return log
def get_repos_path():
sa = get_session()
q = sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
return q.ui_value
@task(ignore_result=True)
@locked_task
def whoosh_index(repo_location, full_index):
from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
#log = whoosh_index.get_logger()
index_location = config['index_dir']
WhooshIndexingDaemon(index_location=index_location,
repo_location=repo_location, sa=get_session())\
@@ -111,13 +106,12 @@ def get_commits_stats(repo_name, ts_min_
lock = l = DaemonLock(file_=jn(lockkey_path, lockkey))
#for js data compatibilty cleans the key for person from '
# for js data compatibilty cleans the key for person from '
akc = lambda k: person(k).replace('"', "")
co_day_auth_aggr = {}
commits_by_day_aggregate = {}
repos_path = get_repos_path()
repo = get_repo(safe_str(os.path.join(repos_path, repo_name)))
repo = Repository.get_by_repo_name(repo_name).scm_instance
repo_size = len(repo.revisions)
#return if repo have no revisions
if repo_size < 1:
@@ -139,9 +133,9 @@ def get_commits_stats(repo_name, ts_min_
last_rev = cur_stats.stat_on_revision
if last_rev == repo.get_changeset().revision and repo_size > 1:
#pass silently without any work if we're not on first revision or
#current state of parsing revision(from db marker) is the
#last revision
# pass silently without any work if we're not on first revision or
# current state of parsing revision(from db marker) is the
# last revision
lock.release()
return True
@@ -255,10 +249,11 @@ def get_commits_stats(repo_name, ts_min_
def send_password_link(user_email):
from rhodecode.model.notification import EmailNotificationModel
log = get_logger(send_password_link)
user = User.get_by_email(user_email)
if user:
@@ -283,9 +278,9 @@ def send_password_link(user_email):
def reset_user_password(user_email):
log = get_logger(reset_user_password)
from rhodecode.lib import auth
@@ -361,27 +356,39 @@ def send_email(recipients, subject, body
def create_repo_fork(form_data, cur_user):
Creates a fork of repository using interval VCS methods
:param form_data:
:param cur_user:
log = get_logger(create_repo_fork)
Session = get_session()
base_path = Repository.base_path()
RepoModel(Session).create(form_data, cur_user, just_db=True, fork=True)
alias = form_data['repo_type']
org_repo_name = form_data['org_path']
source_repo_path = os.path.join(base_path, org_repo_name)
destination_fork_path = os.path.join(base_path, form_data['repo_name_full'])
repo_model = RepoModel(get_session())
repo_model.create(form_data, cur_user, just_db=True, fork=True)
repo_name = form_data['repo_name']
repo_path = os.path.join(repos_path, repo_name)
repo_fork_path = os.path.join(repos_path, form_data['fork_name'])
log.info('creating repo fork %s as %s', repo_name, repo_path)
log.info('creating fork of %s as %s', source_repo_path,
destination_fork_path)
backend = get_backend(alias)
backend(str(repo_fork_path), create=True, src_url=str(repo_path))
backend(safe_str(destination_fork_path), create=True,
src_url=safe_str(source_repo_path))
action_logger(cur_user, 'user_forked_repo:%s' % org_repo_name,
org_repo_name, '', Session)
# finally commit at latest possible stage
Session.commit()
def __get_codes_stats(repo_name):
tip = repo.get_changeset()
code_stats = {}
@@ -33,7 +33,8 @@ from rhodecode.lib.utils import action_l
def repo_size(ui, repo, hooktype=None, **kwargs):
"""Presents size of repository after push
Presents size of repository after push
:param ui:
:param repo:
@@ -65,7 +66,8 @@ def repo_size(ui, repo, hooktype=None, *
def log_pull_action(ui, repo, **kwargs):
"""Logs user last pull action
Logs user last pull action
@@ -76,13 +78,15 @@ def log_pull_action(ui, repo, **kwargs):
repository = extra_params['repository']
action = 'pull'
action_logger(username, action, repository, extra_params['ip'])
action_logger(username, action, repository, extra_params['ip'],
commit=True)
return 0
def log_push_action(ui, repo, **kwargs):
"""Maps user last push action to new changeset id, from mercurial
Maps user last push action to new changeset id, from mercurial
@@ -110,6 +114,7 @@ def log_push_action(ui, repo, **kwargs):
action = action % ','.join(revs)
@@ -93,7 +93,7 @@ def get_repo_slug(request):
return request.environ['pylons.routes_dict'].get('repo_name')
def action_logger(user, action, repo, ipaddr='', sa=None):
def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
Action logger for various actions made by users
@@ -138,12 +138,13 @@ def action_logger(user, action, repo, ip
user_log.action_date = datetime.datetime.now()
user_log.user_ip = ipaddr
sa.add(user_log)
sa.commit()
log.info('Adding user %s, action %s on %s', user_obj, action, repo)
if commit:
sa.rollback()
raise
def get_repos(path, recursive=False):
@@ -185,7 +185,7 @@ class ValidPassword(formencode.validator
class ValidPasswordsMatch(formencode.validators.FancyValidator):
def validate_python(self, value, state):
pass_val = value.get('password') or value.get('new_password')
if pass_val != value['password_confirmation']:
e_dict = {'password_confirmation':
@@ -198,7 +198,7 @@ class ValidAuth(formencode.validators.Fa
'invalid_login':_('invalid user name'),
'disabled_account':_('Your account is disabled')
}
# error mapping
e_dict = {'username':messages['invalid_login'],
'password':messages['invalid_password']}
@@ -208,7 +208,7 @@ class ValidAuth(formencode.validators.Fa
password = value['password']
username = value['username']
user = User.get_by_username(username)
if authenticate(username, password):
return value
@@ -254,7 +254,7 @@ def ValidRepoName(edit, old_data):
# db key This is an actual just the name to store in the
# database
repo_name_full = group_path + RepoGroup.url_sep() + repo_name
group_path = ''
repo_name_full = repo_name
@@ -289,24 +289,8 @@ def ValidRepoName(edit, old_data):
return _ValidRepoName
def ValidForkName():
class _ValidForkName(formencode.validators.FancyValidator):
def to_python(self, value, state):
repo_name = value.get('fork_name')
slug = repo_name_slug(repo_name)
if slug in [ADMIN_PREFIX, '']:
e_dict = {'repo_name': _('This repository name is disallowed')}
raise formencode.Invalid('', value, state, error_dict=e_dict)
if RepoModel().get_by_repo_name(repo_name):
e_dict = {'fork_name':_('This repository '
'already exists')}
raise formencode.Invalid('', value, state,
error_dict=e_dict)
return _ValidForkName
def ValidForkName(*args, **kwargs):
return ValidRepoName(*args, **kwargs)
def SlugifyName():
@@ -513,7 +497,7 @@ def UserForm(edit=False, old_data={}):
password = All(UnicodeString(strip=True, min=6, not_empty=True))
password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=False))
active = StringBoolean(if_missing=False)
name = UnicodeString(strip=True, min=1, not_empty=True)
lastname = UnicodeString(strip=True, min=1, not_empty=True)
@@ -605,17 +589,20 @@ def RepoForm(edit=False, old_data={}, su
chained_validators = [ValidRepoName(edit, old_data), ValidPerms]
return _RepoForm
def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
repo_groups=[]):
class _RepoForkForm(formencode.Schema):
allow_extra_fields = True
filter_extra_fields = False
fork_name = All(UnicodeString(strip=True, min=1, not_empty=True),
repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
SlugifyName())
repo_group = OneOf(repo_groups, hideList=True)
repo_type = All(ValidForkType(old_data), OneOf(supported_backends))
description = UnicodeString(strip=True, min=1, not_empty=True)
private = StringBoolean(if_missing=False)
chained_validators = [ValidForkName()]
copy_permissions = StringBoolean(if_missing=False)
fork_parent_id = UnicodeString()
chained_validators = [ValidForkName(edit, old_data)]
return _RepoForkForm
@@ -630,7 +617,7 @@ def RepoSettingsForm(edit=False, old_dat
chained_validators = [ValidRepoName(edit, old_data), ValidPerms,
ValidSettings]
@@ -35,8 +35,6 @@ from pylons.i18n.translation import _
from rhodecode.model import BaseModel
from rhodecode.model.db import Notification, User, UserNotification
from rhodecode.lib.celerylib import run_task
from rhodecode.lib.celerylib.tasks import send_email
@@ -74,6 +72,7 @@ class NotificationModel(BaseModel):
:param recipients: list of int, str or User objects
:param type_: type of notification
from rhodecode.lib.celerylib import tasks, run_task
if not getattr(recipients, '__iter__', False):
raise Exception('recipients must be a list of iterable')
@@ -100,7 +99,7 @@ class NotificationModel(BaseModel):
email_body_html = EmailNotificationModel()\
.get_email_tmpl(type_, **{'subject':subject,
'body':h.rst(body)})
run_task(send_email, rec.email, email_subject, email_body,
run_task(tasks.send_email, rec.email, email_subject, email_body,
email_body_html)
return notif
@@ -212,36 +212,33 @@ class RepoModel(BaseModel):
def create(self, form_data, cur_user, just_db=False, fork=False):
if fork:
repo_name = form_data['fork_name']
org_name = form_data['repo_name']
org_full_name = org_name
fork_parent_id = form_data['fork_parent_id']
org_name = repo_name = form_data['repo_name']
repo_name_full = form_data['repo_name_full']
# repo name is just a name of repository
# while repo_name_full is a full qualified name that is combined
# with name and path of group
new_repo = Repository()
new_repo.enable_statistics = False
for k, v in form_data.items():
if k == 'repo_name':
v = repo_name
v = repo_name_full
if k == 'repo_group':
k = 'group_id'
if k == 'description':
v = v or repo_name
setattr(new_repo, k, v)
parent_repo = self.sa.query(Repository)\
.filter(Repository.repo_name == org_full_name).one()
parent_repo = Repository.get(fork_parent_id)
new_repo.fork = parent_repo
new_repo.user_id = cur_user.user_id
@@ -271,19 +268,21 @@ class RepoModel(BaseModel):
form_data['repo_group'],
form_data['clone_uri'])
self.sa.commit()
#now automatically start following this repository as owner
# now automatically start following this repository as owner
ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
cur_user.user_id)
return new_repo
self.sa.rollback()
def create_fork(self, form_data, cur_user):
Simple wrapper into executing celery task for fork creation
run_task(tasks.create_repo_fork, form_data, cur_user)
@@ -325,6 +324,11 @@ class RepoModel(BaseModel):
def delete_stats(self, repo_name):
removes stats for given repo
obj = self.sa.query(Statistics)\
.filter(Statistics.repository == \
@@ -208,17 +208,14 @@ class ScmModel(BaseModel):
.filter(UserFollowing.user_id == user_id).scalar()
if f is not None:
self.sa.delete(f)
action_logger(UserTemp(user_id),
'stopped_following_repo',
RepoTemp(follow_repo_id))
return
@@ -226,13 +223,12 @@ class ScmModel(BaseModel):
f.user_id = user_id
f.follows_repo_id = follow_repo_id
self.sa.add(f)
'started_following_repo',
def toggle_following_user(self, follow_user_id, user_id):
@@ -243,11 +239,9 @@ class ScmModel(BaseModel):
@@ -255,10 +249,8 @@ class ScmModel(BaseModel):
f.follows_user_id = follow_user_id
def is_following_repo(self, repo_name, user_id, cache=False):
@@ -317,8 +309,8 @@ class ScmModel(BaseModel):
def commit_change(self, repo, repo_name, cs, user, author, message, content,
f_path):
def commit_change(self, repo, repo_name, cs, user, author, message,
content, f_path):
if repo.alias == 'hg':
from vcs.backends.hg import MercurialInMemoryChangeset as IMC
@@ -1790,6 +1790,10 @@ div.form div.fields div.field div.button
padding: 0 !important;
.trending_language_tbl,.trending_language_tbl tr {
border-spacing: 1px;
.trending_language {
background-color: #003367;
color: #FFF;
@@ -1797,7 +1801,7 @@ div.form div.fields div.field div.button
min-width: 20px;
text-decoration: none;
height: 12px;
margin-bottom: 4px;
margin-bottom: 0px;
margin-left: 5px;
white-space: pre;
padding: 3px;
file renamed from rhodecode/templates/settings/repo_fork.html to rhodecode/templates/forks/fork.html
@@ -27,14 +27,23 @@
<!-- fields -->
<div class="fields">
<div class="field">
<div class="label">
<label for="repo_name">${_('Fork name')}:</label>
</div>
<div class="input">
${h.text('fork_name',class_="small")}
${h.hidden('repo_type',c.repo_info.repo_type)}
${h.text('repo_name',class_="small")}
${h.hidden('fork_parent_id',c.repo_info.repo_id)}
<label for="repo_group">${_('Repository group')}:</label>
${h.select('repo_group','',c.repo_groups,class_="medium")}
<div class="label label-textarea">
<label for="description">${_('Description')}:</label>
@@ -50,7 +59,15 @@
<div class="checkboxes">
${h.checkbox('private',value="True")}
<div class="label label-checkbox">
<label for="private">${_('Copy permissions')}:</label>
${h.checkbox('copy_permissions',value="True")}
<div class="buttons">
${h.submit('',_('fork this repository'),class_="ui-button")}
Status change: