release: Merge default into stable for release preparation

This commit is contained in:
Milka Kuzminski 2021-04-05 19:29:39 +00:00
commit 7c2235e318
55 changed files with 576 additions and 98 deletions

View file

@ -1,6 +1,5 @@
[bumpversion]
current_version = 4.24.1
current_version = 4.25.0
message = release: Bump version {current_version} to {new_version}
[bumpversion:file:rhodecode/VERSION]

View file

@ -5,26 +5,21 @@ done = false
done = true
[task:rc_tools_pinned]
done = true
[task:fixes_on_stable]
done = true
[task:pip2nix_generated]
done = true
[task:changelog_updated]
done = true
[task:generate_api_docs]
done = true
[release]
state = prepared
version = 4.24.1
[task:updated_translation]
[release]
state = in_progress
version = 4.25.0
[task:generate_js_routes]
[task:updated_trial_license]

View file

@ -130,6 +130,24 @@ file_store_get_info (EE only)
error : null
file_store_delete (EE only)
---------------------------
.. py:function:: file_store_delete(apiuser, store_fid)
Delete an artifact based on the secret uuid.
Example output:
.. code-block:: bash
id : <id_given_in_input>
result: {
"artifact" : {"uid": "some uid", "removed": true}
}
error : null
file_store_add_metadata (EE only)
---------------------------------

View file

@ -0,0 +1,76 @@
|RCE| 4.25.0 |RNS|
------------------
Release Date
^^^^^^^^^^^^
- 2021-04-02
New Features
^^^^^^^^^^^^
- SSH: allow clone by ID via SSH operations.
- Artifacts: added an admin panel to manage artifacts.
- Redmine: added option to add note to a ticket without changing its status in Redmine integration.
General
^^^^^^^
- Git: change lookups logic. Prioritize reference names over numerical ids.
Numerical ids are supported as a fallback if ref matching is unsuccessful.
- Permissions: changed fork permission help text to reflect the actual state on how it works.
- Permissions: flush permissions on owner changes for repo and repo groups. This
would fix problems when owner of repository changes then the new owner lacked permissions
until cache expired.
- Artifacts: added API function to remove artifacts.
- Archives: use a special name for non-hashed archives to fix caching issues.
- Packaging: fixed few packages requirements for a proper builds.
- Packaging: fix rhodecode-tools for docker builds.
- Packaging: fixed some problem after latest setuptools-scm release.
- Packaging: added setuptools-scm to packages for build.
- Packaging: fix jira package for reproducible builds.
- Packaging: fix zipp package patches.
Security
^^^^^^^^
- Comments: forbid removal of comments by anyone except the owners.
Previously admins of a repository could remove them if they would construct a special url with data.
- Pull requests: fixed some xss problems when a deleted file with special characters were commented on.
Performance
^^^^^^^^^^^
- License: skip channelstream connect on license checks logic to reduce calls handling times.
- Core: optimize some calls to skip license/scm detection on them. Each license check is expensive
and we don't need them on each call.
Fixes
^^^^^
- Branch-permissions: fixed ce view. Fixes #5656
- Feed: fix errors on feed access of empty repositories.
- Archives: if implicit ref name was used (e.g master) to obtain archive, we now
redirect to explicit commit sha so we can have the proper caching for references names.
- rcextensions: fixed pre-files extractor return code support.
- Svn: fix subprocess problems on some of the calls for file checking.
- Pull requests: fixed multiple repetitions of referenced tickets in pull requests summary sidebar.
- Maintenance: fixed bad routes def
- clone-uri: fixed the problems with key mismatch that caused errors on summary page.
- Largefiles: added fix for downloading largefiles which had no extension in file name.
- Compare: fix referenced commits bug.
- Git: fix for unicode branches
Upgrade notes
^^^^^^^^^^^^^
- Scheduled release 4.25.0.

View file

@ -9,6 +9,7 @@ Release Notes
.. toctree::
:maxdepth: 1
release-notes-4.25.0.rst
release-notes-4.24.1.rst
release-notes-4.24.0.rst
release-notes-4.23.2.rst

View file

@ -0,0 +1,13 @@
diff -rup channelstream-0.6.14-orig/setup.py channelstream-0.6.14/setup.py
--- channelstream-0.6.14/setup-orig.py 2021-03-11 12:34:45.000000000 +0100
+++ channelstream-0.6.14/setup.py 2021-03-11 12:34:56.000000000 +0100
@@ -52,7 +52,7 @@ setup(
include_package_data=True,
install_requires=requires,
python_requires=">=2.7",
- setup_requires=["pytest-runner"],
+ setup_requires=["pytest-runner==5.1.0"],
extras_require={
"dev": ["coverage", "pytest", "pyramid", "tox", "mock", "webtest"],
"lint": ["black"],

View file

@ -0,0 +1,10 @@
diff -rup configparser-4.0.2-orig/pyproject.toml configparser-4.0.2/pyproject.toml
--- configparser-4.0.2-orig/pyproject.toml 2021-03-22 21:28:11.000000000 +0100
+++ configparser-4.0.2/pyproject.toml 2021-03-22 21:28:11.000000000 +0100
@@ -1,5 +1,5 @@
[build-system]
-requires = ["setuptools>=40.7", "wheel", "setuptools_scm>=1.15"]
+requires = ["setuptools<=42.0", "wheel", "setuptools_scm<6.0.0"]
build-backend = "setuptools.build_meta"
[tool.black]

View file

@ -0,0 +1,7 @@
diff -rup importlib-metadata-1.6.0-orig/yproject.toml importlib-metadata-1.6.0/pyproject.toml
--- importlib-metadata-1.6.0-orig/yproject.toml 2021-03-22 22:10:33.000000000 +0100
+++ importlib-metadata-1.6.0/pyproject.toml 2021-03-22 22:11:09.000000000 +0100
@@ -1,3 +1,3 @@
[build-system]
-requires = ["setuptools>=30.3", "wheel", "setuptools_scm"]
+requires = ["setuptools<42.0", "wheel", "setuptools_scm<6.0.0"]

View file

@ -0,0 +1,12 @@
diff -rup pyramid-apispec-0.3.2-orig/setup.py pyramid-apispec-0.3.2/setup.py
--- pyramid-apispec-0.3.2-orig/setup.py 2021-03-11 11:19:26.000000000 +0100
+++ pyramid-apispec-0.3.2/setup.py 2021-03-11 11:19:51.000000000 +0100
@@ -44,7 +44,7 @@ setup(
packages=find_packages(exclude=["contrib", "docs", "tests"]),
package_data={"pyramid_apispec": ["static/*.*"], "": ["LICENSE"]},
install_requires=["apispec[yaml]==1.0.0"],
- setup_requires=["pytest-runner"],
+ setup_requires=["pytest-runner==5.1"],
extras_require={
"dev": ["coverage", "pytest", "pyramid", "tox", "webtest"],
"demo": ["marshmallow==2.15.3", "pyramid", "apispec", "webtest"],

View file

@ -6,7 +6,7 @@ diff -rup pytest-4.6.5-orig/setup.py pytest-4.6.5/setup.py
setup(
use_scm_version={"write_to": "src/_pytest/_version.py"},
- setup_requires=["setuptools-scm", "setuptools>=40.0"],
+ setup_requires=["setuptools-scm", "setuptools<=42.0"],
+ setup_requires=["setuptools-scm<6.0.0", "setuptools<=42.0"],
package_dir={"": "src"},
# fmt: off
extras_require={

View file

@ -0,0 +1,12 @@
diff -rup rhodecode-tools-1.4.0-orig/setup.py rhodecode-tools-1.4.0/setup.py
--- rhodecode-tools-1.4.0/setup-orig.py 2021-03-11 12:34:45.000000000 +0100
+++ rhodecode-tools-1.4.0/setup.py 2021-03-11 12:34:56.000000000 +0100
@@ -69,7 +69,7 @@ def _get_requirements(req_filename, excl
# requirements extract
-setup_requirements = ['pytest-runner']
+setup_requirements = ['pytest-runner==5.1.0']
install_requirements = _get_requirements(
'requirements.txt', exclude=['setuptools'])
test_requirements = _get_requirements('requirements_test.txt')

View file

@ -0,0 +1,10 @@
diff -rup zip-1.2.0-orig/pyproject.toml zip-1.2.0/pyproject.toml
--- zip-1.2.0-orig/pyproject.toml 2021-03-23 10:55:37.000000000 +0100
+++ zip-1.2.0/pyproject.toml 2021-03-23 10:56:05.000000000 +0100
@@ -1,5 +1,5 @@
[build-system]
-requires = ["setuptools>=34.4", "wheel", "setuptools_scm>=1.15"]
+requires = ["setuptools<42.0", "wheel", "setuptools_scm<6.0.0"]
build-backend = "setuptools.build_meta"
[tool.black]

View file

@ -280,6 +280,72 @@ self: super: {
];
});
"pytest-runner" = super."pytest-runner".override (attrs: {
propagatedBuildInputs = [
self."setuptools-scm"
];
});
"py" = super."py".override (attrs: {
propagatedBuildInputs = [
self."setuptools-scm"
];
});
"python-dateutil" = super."python-dateutil".override (attrs: {
propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
self."setuptools-scm"
];
});
"configparser" = super."configparser".override (attrs: {
patches = [
./patches/configparser/pyproject.patch
];
propagatedBuildInputs = [
self."setuptools-scm"
];
});
"importlib-metadata" = super."importlib-metadata".override (attrs: {
patches = [
./patches/importlib_metadata/pyproject.patch
];
propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
self."setuptools-scm"
];
});
"zipp" = super."zipp".override (attrs: {
patches = [
./patches/zipp/pyproject.patch
];
propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
self."setuptools-scm"
];
});
"pyramid-apispec" = super."pyramid-apispec".override (attrs: {
patches = [
./patches/pyramid_apispec/setuptools.patch
];
});
"channelstream" = super."channelstream".override (attrs: {
patches = [
./patches/channelstream/setuptools.patch
];
});
"rhodecode-tools" = super."rhodecode-tools".override (attrs: {
patches = [
./patches/rhodecode_tools/setuptools.patch
];
});
# Avoid that base packages screw up the build process
inherit (basePythonPackages)
setuptools;

View file

@ -1883,7 +1883,7 @@ self: super: {
};
};
"rhodecode-enterprise-ce" = super.buildPythonPackage {
name = "rhodecode-enterprise-ce-4.24.1";
name = "rhodecode-enterprise-ce-4.25.0";
buildInputs = [
self."pytest"
self."py"
@ -2092,6 +2092,17 @@ self: super: {
license = [ pkgs.lib.licenses.mit ];
};
};
"setuptools-scm" = super.buildPythonPackage {
name = "setuptools-scm-3.5.0";
doCheck = false;
src = fetchurl {
url = "https://files.pythonhosted.org/packages/b2/f7/60a645aae001a2e06cf4b8db2fba9d9f36b8fd378f10647e3e218b61b74b/setuptools_scm-3.5.0.tar.gz";
sha256 = "5bdf21a05792903cafe7ae0c9501182ab52497614fa6b1750d9dbae7b60c1a87";
};
meta = {
license = [ pkgs.lib.licenses.psfl ];
};
};
"simplegeneric" = super.buildPythonPackage {
name = "simplegeneric-0.8.1";
doCheck = false;

View file

@ -1 +1 @@
4.24.1
4.25.0

View file

@ -1212,7 +1212,7 @@ def fork_repo(request, apiuser, repoid, fork_name,
validate_repo_permissions(apiuser, repoid, repo, _perms)
# check if the regular user has at least fork permissions as well
if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
if not HasPermissionAnyApi(PermissionModel.FORKING_ENABLED)(user=apiuser):
raise JSONRPCForbidden()
# check if user can set owner parameter

View file

@ -255,7 +255,7 @@ class LocalFileStorage(object):
return filename, metadata
def get_metadata(self, filename):
def get_metadata(self, filename, ignore_missing=False):
"""
Reads JSON stored metadata for a file
@ -264,6 +264,7 @@ class LocalFileStorage(object):
"""
filename = self.store_path(filename)
filename_meta = filename + '.meta'
if ignore_missing and not os.path.isfile(filename_meta):
return {}
with open(filename_meta, "rb") as source_meta:
return json.loads(source_meta.read())

View file

@ -932,8 +932,8 @@ def includeme(config):
name='edit_repo_perms_branch',
pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
config.add_view(
RepoBranchesView,
attr='branches',
RepoSettingsBranchPermissionsView,
attr='branch_permissions',
route_name='edit_repo_perms_branch', request_method='GET',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
@ -950,8 +950,8 @@ def includeme(config):
config.add_view(
RepoMaintenanceView,
attr='repo_maintenance',
route_name='edit_repo_maintenance_execute', request_method='GET',
renderer='json', xhr=True)
route_name='edit_repo_maintenance', request_method='GET',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
config.add_route(
name='edit_repo_maintenance_execute',
@ -959,8 +959,8 @@ def includeme(config):
config.add_view(
RepoMaintenanceView,
attr='repo_maintenance_execute',
route_name='edit_repo_maintenance', request_method='GET',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
route_name='edit_repo_maintenance_execute', request_method='GET',
renderer='json', xhr=True)
# Fields
config.add_route(

View file

@ -542,6 +542,28 @@ class TestRepositoryArchival(object):
for header in headers:
assert header in response.headers.items()
def test_archival_no_hash(self, backend):
backend.enable_downloads()
commit = backend.repo.get_commit(commit_idx=173)
for a_type, content_type, extension in settings.ARCHIVE_SPECS:
short = 'plain' + extension
fname = commit.raw_id + extension
filename = '%s-%s' % (backend.repo_name, short)
response = self.app.get(
route_path('repo_archivefile',
repo_name=backend.repo_name,
fname=fname, params={'with_hash': 0}))
assert response.status == '200 OK'
headers = [
('Content-Disposition', 'attachment; filename=%s' % filename),
('Content-Type', '%s' % content_type),
]
for header in headers:
assert header in response.headers.items()
@pytest.mark.parametrize('arch_ext',[
'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
def test_archival_wrong_ext(self, backend, arch_ext):

View file

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2020 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# 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 Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import mock
import pytest
from rhodecode.lib.utils2 import str2bool
from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
from rhodecode.model.meta import Session
from rhodecode.tests import (
TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
from rhodecode.tests.fixture import Fixture
fixture = Fixture()
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'edit_repo_maintenance': '/{repo_name}/settings/maintenance',
'edit_repo_maintenance_execute': '/{repo_name}/settings/maintenance/execute',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
def _get_permission_for_user(user, repo):
perm = UserRepoToPerm.query()\
.filter(UserRepoToPerm.repository ==
Repository.get_by_repo_name(repo))\
.filter(UserRepoToPerm.user == User.get_by_username(user))\
.all()
return perm
@pytest.mark.usefixtures('autologin_user', 'app')
class TestAdminRepoMaintenance(object):
@pytest.mark.parametrize('urlname', [
'edit_repo_maintenance',
])
def test_show_page(self, urlname, app, backend):
app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
def test_execute_maintenance_for_repo_hg(self, app, backend_hg, autologin_user, xhr_header):
repo_name = backend_hg.repo_name
response = app.get(
route_path('edit_repo_maintenance_execute',
repo_name=repo_name,),
extra_environ=xhr_header)
assert "HG Verify repo" in ''.join(response.json)

View file

@ -34,7 +34,7 @@ from rhodecode.lib.auth import (
from rhodecode.lib.ext_json import json
from rhodecode.lib.graphmod import _colored, _dagwalker
from rhodecode.lib.helpers import RepoPage
from rhodecode.lib.utils2 import safe_int, safe_str, str2bool
from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
from rhodecode.lib.vcs.exceptions import (
RepositoryError, CommitDoesNotExistError,
CommitError, NodeDoesNotExistError, EmptyRepositoryError)
@ -110,7 +110,7 @@ class RepoChangelogView(RepoAppView):
def _check_if_valid_branch(self, branch_name, repo_name, f_path):
if branch_name not in self.rhodecode_vcs_repo.branches_all:
h.flash('Branch {} is not found.'.format(h.escape(branch_name)),
h.flash(u'Branch {} is not found.'.format(h.escape(safe_unicode(branch_name))),
category='warning')
redirect_url = h.route_path(
'repo_commits_file', repo_name=repo_name,

View file

@ -674,6 +674,10 @@ class RepoCommitsView(RepoAppView):
is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
comment_repo_admin = is_repo_admin and is_repo_comment
if comment.draft and not comment_owner:
# We never allow to delete draft comments for other than owners
raise HTTPNotFound()
if super_admin or comment_owner or comment_repo_admin:
CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
Session().commit()

View file

@ -104,6 +104,9 @@ class RepoFeedView(RepoAppView):
def _get_commits(self):
pre_load = ['author', 'branch', 'date', 'message', 'parents']
if self.rhodecode_vcs_repo.is_empty():
return []
collection = self.rhodecode_vcs_repo.get_commits(
branch_name=None, show_hidden=False, pre_load=pre_load,
translate_tags=False)
@ -137,6 +140,7 @@ class RepoFeedView(RepoAppView):
language=self.language,
ttl=self.ttl
)
for commit in reversed(self._get_commits()):
date = self._set_timezone(commit.date)
feed.add_item(

View file

@ -325,17 +325,18 @@ class RepoFilesView(RepoAppView):
return lf_enabled
def _get_archive_name(self, db_repo_name, commit_sha, ext, subrepos=False, path_sha=''):
def _get_archive_name(self, db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
# original backward compat name of archive
clean_name = safe_str(db_repo_name.replace('/', '_'))
# e.g vcsserver.zip
# e.g vcsserver-abcdefgh.zip
# e.g vcsserver-abcdefgh-defghijk.zip
archive_name = '{}{}{}{}{}'.format(
archive_name = '{}{}{}{}{}{}'.format(
clean_name,
'-sub' if subrepos else '',
commit_sha,
'-{}'.format('plain') if not with_hash else '',
'-{}'.format(path_sha) if path_sha else '',
ext)
return archive_name
@ -372,6 +373,11 @@ class RepoFilesView(RepoAppView):
except EmptyRepositoryError:
return Response(_('Empty repository'))
# we used a ref, or a shorter version, lets redirect client ot use explicit hash
if commit_id != commit.raw_id:
fname='{}{}'.format(commit.raw_id, ext)
raise HTTPFound(self.request.current_route_path(fname=fname))
try:
at_path = commit.get_node(at_path).path or default_at_path
except Exception:
@ -385,7 +391,7 @@ class RepoFilesView(RepoAppView):
# used for cache etc
archive_name = self._get_archive_name(
self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
path_sha=path_sha)
path_sha=path_sha, with_hash=with_hash)
if not with_hash:
short_sha = ''
@ -394,7 +400,7 @@ class RepoFilesView(RepoAppView):
# what end client gets served
response_archive_name = self._get_archive_name(
self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
path_sha=path_sha)
path_sha=path_sha, with_hash=with_hash)
# remove extension from our archive directory name
archive_dir_name = response_archive_name[:-len(ext)]
@ -404,9 +410,10 @@ class RepoFilesView(RepoAppView):
cached_archive_path = None
if archive_cache_enabled:
# check if we it's ok to write
# check if we it's ok to write, and re-create the archive cache
if not os.path.isdir(CONFIG['archive_cache_dir']):
os.makedirs(CONFIG['archive_cache_dir'])
cached_archive_path = os.path.join(
CONFIG['archive_cache_dir'], archive_name)
if os.path.isfile(cached_archive_path):

View file

@ -165,7 +165,7 @@ class RepoForksView(RepoAppView, DataGridAppView):
@LoginRequired()
@NotAnonymous()
@HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
@HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def repo_fork_new(self):
@ -191,7 +191,7 @@ class RepoForksView(RepoAppView, DataGridAppView):
@LoginRequired()
@NotAnonymous()
@HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
@HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@CSRFRequired()

View file

@ -1748,6 +1748,10 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
is_repo_comment = comment.repo.repo_name == self.db_repo_name
comment_repo_admin = is_repo_admin and is_repo_comment
if comment.draft and not comment_owner:
# We never allow to delete draft comments for other than owners
raise HTTPNotFound()
if super_admin or comment_owner or comment_repo_admin:
old_calculated_status = comment.pull_request.calculated_review_status()
CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)

View file

@ -89,6 +89,14 @@ class SshWrapper(object):
return conn
def maybe_translate_repo_uid(self, repo_name):
if repo_name.startswith('_'):
from rhodecode.model.repo import RepoModel
by_id_match = RepoModel().get_repo_by_id(repo_name)
if by_id_match:
repo_name = by_id_match.repo_name
return repo_name
def get_repo_details(self, mode):
vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
repo_name = None
@ -97,14 +105,14 @@ class SshWrapper(object):
hg_match = re.match(hg_pattern, self.command)
if hg_match is not None:
vcs_type = 'hg'
repo_name = hg_match.group(1).strip('/')
repo_name = self.maybe_translate_repo_uid(hg_match.group(1).strip('/'))
return vcs_type, repo_name, mode
git_pattern = r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$'
git_match = re.match(git_pattern, self.command)
if git_match is not None:
vcs_type = 'git'
repo_name = git_match.group(2).strip('/')
repo_name = self.maybe_translate_repo_uid(git_match.group(2).strip('/'))
mode = git_match.group(1)
return vcs_type, repo_name, mode

View file

@ -19,6 +19,8 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/
import json
import os
import mock
import pytest
@ -107,6 +109,8 @@ class TestGitServer(object):
def test_run_returns_executes_command(self, git_server):
server = git_server.create()
from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper
os.environ['SSH_CLIENT'] = '127.0.0.1'
with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch:
_patch.return_value = 0
with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'):

View file

@ -108,6 +108,7 @@ class TestMercurialServer(object):
def test_run_returns_executes_command(self, hg_server):
server = hg_server.create()
from rhodecode.apps.ssh_support.lib.backends.hg import MercurialTunnelWrapper
os.environ['SSH_CLIENT'] = '127.0.0.1'
with mock.patch.object(MercurialTunnelWrapper, 'create_hooks_env') as _patch:
_patch.return_value = 0
with mock.patch.object(MercurialTunnelWrapper, 'command', return_value='date'):

View file

@ -17,7 +17,7 @@
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import os
import mock
import pytest
@ -174,6 +174,7 @@ class TestSubversionServer(object):
def test_run_returns_executes_command(self, svn_server):
server = svn_server.create()
from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
os.environ['SSH_CLIENT'] = '127.0.0.1'
with mock.patch.object(
SubversionTunnelWrapper, 'get_first_client_response',
return_value={'url': 'http://server/test-svn'}):

View file

@ -32,6 +32,7 @@ import json
from rhodecode.lib import diffs
from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
from rhodecode.lib.vcs.backends.git.diff import GitDiff
from vcsserver.utils import safe_int
def get_svn_files(repo, vcs_repo, refs):
@ -74,7 +75,7 @@ def get_svn_files(repo, vcs_repo, refs):
# skip dirs
continue
parsed_entry['file_size'] = int(stdout.strip())
parsed_entry['file_size'] = safe_int(stdout.strip()) or 0
files.append(parsed_entry)

View file

@ -524,8 +524,10 @@ class PermissionCalculator(object):
# In case we want to extend this list we should make sure
# this is in sync with User.DEFAULT_USER_PERMISSIONS definitions
from rhodecode.model.permission import PermissionModel
_configurable = frozenset([
'hg.fork.none', 'hg.fork.repository',
PermissionModel.FORKING_DISABLED, PermissionModel.FORKING_ENABLED,
'hg.create.none', 'hg.create.repository',
'hg.usergroup.create.false', 'hg.usergroup.create.true',
'hg.repogroup.create.false', 'hg.repogroup.create.true',

View file

@ -25,6 +25,7 @@ Consists of functions to typically be used within templates, but also
available to Controllers. This module is available to both as 'h'.
"""
import base64
import collections
import os
import random
@ -1733,7 +1734,7 @@ def process_patterns(text_string, repo_name, link_format='html', active_entries=
def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
issues_container=None, error_container=None):
issues_container_callback=None, error_container=None):
"""
Parses given text message and makes proper links.
issues are linked to given issue-server, and rest is a commit link
@ -1756,8 +1757,9 @@ def urlify_commit_message(commit_text, repository=None, active_pattern_entries=N
new_text, issues, errors = process_patterns(
new_text, repository or '', active_entries=active_pattern_entries)
if issues_container is not None:
issues_container.extend(issues)
if issues_container_callback is not None:
for issue in issues:
issues_container_callback(issue)
if error_container is not None:
error_container.extend(errors)
@ -1802,7 +1804,7 @@ def renderer_from_filename(filename, exclude=None):
def render(source, renderer='rst', mentions=False, relative_urls=None,
repo_name=None, active_pattern_entries=None, issues_container=None):
repo_name=None, active_pattern_entries=None, issues_container_callback=None):
def maybe_convert_relative_links(html_source):
if relative_urls:
@ -1819,8 +1821,9 @@ def render(source, renderer='rst', mentions=False, relative_urls=None,
source, issues, errors = process_patterns(
source, repo_name, link_format='rst',
active_entries=active_pattern_entries)
if issues_container is not None:
issues_container.extend(issues)
if issues_container_callback is not None:
for issue in issues:
issues_container_callback(issue)
return literal(
'<div class="rst-block">%s</div>' %
@ -1833,8 +1836,10 @@ def render(source, renderer='rst', mentions=False, relative_urls=None,
source, issues, errors = process_patterns(
source, repo_name, link_format='markdown',
active_entries=active_pattern_entries)
if issues_container is not None:
issues_container.extend(issues)
if issues_container_callback is not None:
for issue in issues:
issues_container_callback(issue)
return literal(
'<div class="markdown-block">%s</div>' %
@ -2115,3 +2120,29 @@ def is_active(menu_entry, selected):
if selected in menu_entry:
return "active"
class IssuesRegistry(object):
"""
issue_registry = IssuesRegistry()
some_func(issues_callback=issues_registry(...))
"""
def __init__(self):
self.issues = []
self.unique_issues = collections.defaultdict(lambda: [])
def __call__(self, commit_dict=None):
def callback(issue):
if commit_dict and issue:
issue['commit'] = commit_dict
self.issues.append(issue)
self.unique_issues[issue['id']].append(issue)
return callback
def get_issues(self):
return self.issues
@property
def issues_unique_count(self):
return len(set(i['id'] for i in self.issues))

View file

@ -161,15 +161,28 @@ def detect_vcs_request(environ, backends):
# List of path views first chunk we don't do any checks
white_list = [
# e.g /_file_store/download
'_file_store'
'_file_store',
# static files no detection
'_static',
# full channelstream connect should be VCS skipped
'_admin/channelstream/connect',
]
path_info = environ['PATH_INFO']
if get_path_elem(path_info) in white_list:
path_elem = get_path_elem(path_info)
if path_elem in white_list:
log.debug('path `%s` in whitelist, skipping...', path_info)
return handler
path_url = path_info.lstrip('/')
if path_url in white_list:
log.debug('full url path `%s` in whitelist, skipping...', path_url)
return handler
if VCS_TYPE_KEY in environ:
raw_type = environ[VCS_TYPE_KEY]
if raw_type == VCS_TYPE_SKIP:
@ -181,7 +194,7 @@ def detect_vcs_request(environ, backends):
log.debug('got handler:%s from environ', handler)
if not handler:
log.debug('request start: checking if request is of VCS type in order: %s', backends)
log.debug('request start: checking if request for `%s` is of VCS type in order: %s', path_elem, backends)
for vcs_type in backends:
vcs_check, _handler = checks[vcs_type]
if vcs_check(environ):

View file

@ -594,6 +594,9 @@ def credentials_filter(uri):
:param uri:
"""
import urlobject
if isinstance(uri, rhodecode.lib.encrypt.InvalidDecryptedValue):
return 'InvalidDecryptionKey'
url_obj = urlobject.URLObject(cleaned_uri(uri))
url_obj = url_obj.without_password().without_username()
@ -655,7 +658,7 @@ def get_clone_url(request, uri_tmpl, repo_name, repo_id, repo_type, **override):
def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
maybe_unreachable=False):
maybe_unreachable=False, reference_obj=None):
"""
Safe version of get_commit if this commit doesn't exists for a
repository it returns a Dummy one instead
@ -665,6 +668,7 @@ def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
:param commit_idx: numeric commit index
:param pre_load: optional list of commit attributes to load
:param maybe_unreachable: translate unreachable commits on git repos
:param reference_obj: explicitly search via a reference obj in git. E.g "branch:123" would mean branch "123"
"""
# TODO(skreft): remove these circular imports
from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
@ -676,7 +680,7 @@ def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
try:
commit = repo.get_commit(
commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load,
maybe_unreachable=maybe_unreachable)
maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
except (RepositoryError, LookupError):
commit = EmptyCommit()
return commit

View file

@ -72,6 +72,10 @@ class Reference(_Reference):
if self.type == 'book':
return self.name
@property
def to_unicode(self):
return reference_to_unicode(self)
def unicode_to_reference(raw):
"""
@ -483,7 +487,7 @@ class BaseRepository(object):
self._is_empty = False
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
translate_tag=None, maybe_unreachable=False):
translate_tag=None, maybe_unreachable=False, reference_obj=None):
"""
Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
are both None, most recent commit is returned.

View file

@ -98,7 +98,7 @@ class GitCommit(base.BaseCommit):
elif attr == "parents":
value = self._make_commits(value)
elif attr == "branch":
value = value[0] if value else None
value = self._set_branch(value)
self.__dict__[attr] = value
@LazyProperty
@ -156,14 +156,16 @@ class GitCommit(base.BaseCommit):
branches.append(name)
return branches
@LazyProperty
def branch(self):
branches = self._remote.branch(self.raw_id)
def _set_branch(self, branches):
if branches:
# actually commit can have multiple branches in git
return safe_unicode(branches[0])
@LazyProperty
def branch(self):
branches = self._remote.branch(self.raw_id)
return self._set_branch(branches)
def _get_tree_id_for_path(self, path):
path = safe_str(path)
if path in self._paths:

View file

@ -228,7 +228,8 @@ class GitRepository(BaseRepository):
return []
return output.splitlines()
def _lookup_commit(self, commit_id_or_idx, translate_tag=True, maybe_unreachable=False):
def _lookup_commit(self, commit_id_or_idx, translate_tag=True, maybe_unreachable=False, reference_obj=None):
def is_null(value):
return len(value) == commit_id_or_idx.count('0')
@ -239,21 +240,34 @@ class GitRepository(BaseRepository):
*map(safe_str, [commit_id_or_idx, self.name]))
is_bstr = isinstance(commit_id_or_idx, (str, unicode))
if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
try:
commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
except Exception:
raise CommitDoesNotExistError(commit_missing_err)
is_branch = reference_obj and reference_obj.branch
elif is_bstr:
# Need to call remote to translate id for tagging scenario
lookup_ok = False
if is_bstr:
# Need to call remote to translate id for tagging scenarios,
# or branch that are numeric
try:
remote_data = self._remote.get_object(commit_id_or_idx,
maybe_unreachable=maybe_unreachable)
commit_id_or_idx = remote_data["commit_id"]
lookup_ok = True
except (CommitDoesNotExistError,):
raise CommitDoesNotExistError(commit_missing_err)
lookup_ok = False
if lookup_ok is False:
is_numeric_idx = \
(is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12) \
or isinstance(commit_id_or_idx, int)
if not is_branch and (is_numeric_idx or is_null(commit_id_or_idx)):
try:
commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
lookup_ok = True
except Exception:
raise CommitDoesNotExistError(commit_missing_err)
# we failed regular lookup, and by integer number lookup
if lookup_ok is False:
raise CommitDoesNotExistError(commit_missing_err)
# Ensure we return full id
if not SHA_PATTERN.match(str(commit_id_or_idx)):
@ -413,11 +427,12 @@ class GitRepository(BaseRepository):
return
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
translate_tag=True, maybe_unreachable=False):
translate_tag=True, maybe_unreachable=False, reference_obj=None):
"""
Returns `GitCommit` object representing commit from git repository
at the given `commit_id` or head (most recent commit) if None given.
"""
if self.is_empty():
raise EmptyRepositoryError("There are no commits yet")
@ -443,7 +458,9 @@ class GitRepository(BaseRepository):
commit_id = "tip"
if translate_tag:
commit_id = self._lookup_commit(commit_id, maybe_unreachable=maybe_unreachable)
commit_id = self._lookup_commit(
commit_id, maybe_unreachable=maybe_unreachable,
reference_obj=reference_obj)
try:
idx = self._commit_ids[commit_id]

View file

@ -437,7 +437,7 @@ class MercurialRepository(BaseRepository):
return os.path.join(self.path, '.hg', '.hgrc')
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
translate_tag=None, maybe_unreachable=False):
translate_tag=None, maybe_unreachable=False, reference_obj=None):
"""
Returns ``MercurialCommit`` object representing repository's
commit at the given `commit_id` or `commit_idx`.

View file

@ -277,7 +277,7 @@ class SubversionRepository(base.BaseRepository):
return os.path.join(self.path, 'hooks')
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
translate_tag=None, maybe_unreachable=False):
translate_tag=None, maybe_unreachable=False, reference_obj=None):
if self.is_empty():
raise EmptyRepositoryError("There are no commits yet")
if commit_id is not None:

View file

@ -468,7 +468,7 @@ class FileNode(Node):
mtype, encoding = db.guess_type(self.name)
if mtype is None:
if self.is_binary:
if not self.is_largefile() and self.is_binary:
mtype = 'application/octet-stream'
encoding = None
else:
@ -839,6 +839,7 @@ class LargeFileNode(FileNode):
self.org_path = org_path
self.kind = NodeKind.LARGEFILE
self.alias = alias
self._content = ''
def _validate_path(self, path):
"""

View file

@ -2398,10 +2398,10 @@ class Repository(Base, BaseModel):
# SCM PROPERTIES
#==========================================================================
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
return get_commit_safe(
self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
maybe_unreachable=maybe_unreachable)
maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
def get_changeset(self, rev=None, pre_load=None):
warnings.warn("Use get_commit", DeprecationWarning)

View file

@ -41,6 +41,8 @@ class PermissionModel(BaseModel):
"""
Permissions model for RhodeCode
"""
FORKING_DISABLED = 'hg.fork.none'
FORKING_ENABLED = 'hg.fork.repository'
cls = Permission
global_perms = {
@ -122,8 +124,8 @@ class PermissionModel(BaseModel):
('hg.repogroup.create.true', _('Enabled'))]
c_obj.fork_choices = [
('hg.fork.none', _('Disabled')),
('hg.fork.repository', _('Enabled'))]
(self.FORKING_DISABLED, _('Disabled')),
(self.FORKING_ENABLED, _('Enabled'))]
c_obj.inherit_default_permission_choices = [
('hg.inherit_default_perms.false', _('Disabled')),

View file

@ -908,7 +908,8 @@ class PullRequestModel(BaseModel):
try:
if source_ref_type in self.REF_TYPES:
source_commit = source_repo.get_commit(source_ref_name)
source_commit = source_repo.get_commit(
source_ref_name, reference_obj=pull_request.source_ref_parts)
else:
source_commit = source_repo.get_commit(source_ref_id)
except CommitDoesNotExistError:
@ -922,7 +923,8 @@ class PullRequestModel(BaseModel):
try:
if target_ref_type in self.REF_TYPES:
target_commit = target_repo.get_commit(target_ref_name)
target_commit = target_repo.get_commit(
target_ref_name, reference_obj=pull_request.target_ref_parts)
else:
target_commit = target_repo.get_commit(target_ref_id)
except CommitDoesNotExistError:

View file

@ -46,6 +46,7 @@ from rhodecode.model.db import (
Session, Repository, UserRepoToPerm, UserGroupRepoToPerm,
UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission,
Statistics, UserGroup, RepoGroup, RepositoryField, UserLog)
from rhodecode.model.permission import PermissionModel
from rhodecode.model.settings import VcsSettingsModel
log = logging.getLogger(__name__)
@ -422,8 +423,15 @@ class RepoModel(BaseModel):
try:
cur_repo = self._get_repo(repo)
source_repo_name = cur_repo.repo_name
affected_user_ids = []
if 'user' in kwargs:
cur_repo.user = User.get_by_username(kwargs['user'])
old_owner_id = cur_repo.user.user_id
new_owner = User.get_by_username(kwargs['user'])
cur_repo.user = new_owner
if old_owner_id != new_owner.user_id:
affected_user_ids = [new_owner.user_id, old_owner_id]
if 'repo_group' in kwargs:
cur_repo.group = RepoGroup.get(kwargs['repo_group'])
@ -474,6 +482,9 @@ class RepoModel(BaseModel):
self._rename_filesystem_repo(
old=source_repo_name, new=new_name)
if affected_user_ids:
PermissionModel().trigger_permission_flush(affected_user_ids)
return cur_repo
except Exception:
log.error(traceback.format_exc())

View file

@ -39,6 +39,7 @@ from rhodecode.model import BaseModel
from rhodecode.model.db import (_hash_key, func, or_, in_filter_generator,
Session, RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
UserGroup, Repository)
from rhodecode.model.permission import PermissionModel
from rhodecode.model.settings import VcsSettingsModel, SettingsModel
from rhodecode.lib.caching_query import FromCache
from rhodecode.lib.utils2 import action_logger_generic
@ -531,8 +532,14 @@ class RepoGroupModel(BaseModel):
new_path = repo_group.full_path
affected_user_ids = []
if 'user' in form_data:
repo_group.user = User.get_by_username(form_data['user'])
old_owner_id = repo_group.user.user_id
new_owner = User.get_by_username(form_data['user'])
repo_group.user = new_owner
if old_owner_id != new_owner.user_id:
affected_user_ids = [new_owner.user_id, old_owner_id]
self.sa.add(repo_group)
@ -566,6 +573,9 @@ class RepoGroupModel(BaseModel):
# Trigger update event.
events.trigger(events.RepoGroupUpdateEvent(repo_group))
if affected_user_ids:
PermissionModel().trigger_permission_flush(affected_user_ids)
return repo_group
except Exception:
log.error(traceback.format_exc())

View file

@ -12,6 +12,12 @@
******************************************************************************/
function registerRCRoutes() {
// routes registration
pyroutes.register('admin_artifacts', '/_admin/artifacts', []);
pyroutes.register('admin_artifacts_data', '/_admin/artifacts-data', []);
pyroutes.register('admin_artifacts_delete', '/_admin/artifacts/%(uid)s/delete', ['uid']);
pyroutes.register('admin_artifacts_show_all', '/_admin/artifacts', []);
pyroutes.register('admin_artifacts_show_info', '/_admin/artifacts/%(uid)s', ['uid']);
pyroutes.register('admin_artifacts_update', '/_admin/artifacts/%(uid)s/update', ['uid']);
pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);

View file

@ -1331,7 +1331,7 @@ var CommentsController = function() {
// There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
if ($comments.length===0) {
var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(escapeHtml(f_path), line_no)
var $reply_container = $('#cb-comments-inline-container-template')
$reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
$td.append($($reply_container).html());

View file

@ -115,6 +115,7 @@
<li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
<li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
<li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
<li class="${h.is_active('artifacts', active)}"><a href="${h.route_path('admin_artifacts')}">${_('Artifacts')}</a></li>
<li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
<li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
<li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>

View file

@ -62,7 +62,7 @@
<div class="radios">
${h.radio('default_fork_create' + suffix, c.fork_choices[1][0], label=c.fork_choices[1][1], **kwargs)}
${h.radio('default_fork_create' + suffix, c.fork_choices[0][0], label=c.fork_choices[0][1], **kwargs)}
<span class="help-block">${_('Permission to create root level repository forks. When disabled, users can still fork repositories inside their own repository groups.')}</span>
<span class="help-block">${_('Permission to create repository forks. Root level forks will only work if repository creation is enabled.')}</span>
</div>
</div>
<div class="field">

View file

@ -24,7 +24,7 @@
## to speed up lookups cache some functions before the loop
<%
active_patterns = h.get_active_pattern_entries(c.repo_name)
urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns, issues_container=getattr(c, 'referenced_commit_issues', None))
urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns)
%>
%for commit in c.commit_ranges:
@ -57,7 +57,7 @@
</td>
<td class="mid td-description">
<div class="log-container truncate-wrap">
<div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${urlify_commit_message(commit.message, c.repo_name)}</div>
<div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${urlify_commit_message(commit.message, c.repo_name, issues_container_callback=getattr(c, 'referenced_commit_issues', h.IssuesRegistry())(commit.serialize()))}</div>
</div>
</td>
</tr>

View file

@ -416,6 +416,12 @@ ${h.style_metatag(tag_type, tag)|n,trim}
</a>
</%def>
<%def name="repo_artifact_admin_name(file_uid, artifact_display_name)">
<a href="${h.route_path('admin_artifacts_show_info', uid=file_uid)}">
${(artifact_display_name or '_EMPTY_NAME_')}
</a>
</%def>
<%def name="repo_artifact_uid(repo_name, file_uid)">
<code>${h.shorter(file_uid, size=24, prefix=True)}</code>
</%def>
@ -443,6 +449,7 @@ ${h.style_metatag(tag_type, tag)|n,trim}
% endif
</%def>
<%def name="markup_form(form_id, form_text='', help_text=None)">
<div class="markup-form">

View file

@ -221,7 +221,7 @@ if (show_disabled) {
<%= version_info %>
<% } %>
<br/>
File: <code><%- file_name -%></code>
File: <code><%= file_name -%></code>
<% } else { %>
<% if (review_status) { %>
<i class="icon-circle review-status-<%= review_status %>"></i>

View file

@ -27,8 +27,8 @@
<%def name="main()">
## Container to gather extracted Tickets
<%
c.referenced_commit_issues = []
c.referenced_desc_issues = []
c.referenced_commit_issues = h.IssuesRegistry()
c.referenced_desc_issues = h.IssuesRegistry()
%>
<script type="text/javascript">
@ -86,7 +86,7 @@
</div>
<div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container=c.referenced_desc_issues)}
${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container_callback=c.referenced_desc_issues())}
</div>
<div id="pr-desc-edit" class="input textarea" style="display: none;">
@ -435,7 +435,7 @@
</td>
<td class="mid td-description">
<div class="log-container truncate-wrap">
<div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name, issues_container=c.referenced_commit_issues)}</div>
<div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name, issues_container_callback=c.referenced_commit_issues(commit.serialize()))}</div>
</div>
</td>
</tr>
@ -809,7 +809,7 @@
<div class="sidebar-element clear-both">
<div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
<i class="icon-info-circled"></i>
${(len(c.referenced_desc_issues) + len(c.referenced_commit_issues))}
${(c.referenced_desc_issues.issues_unique_count + c.referenced_commit_issues.issues_unique_count)}
</div>
<div class="right-sidebar-expanded-state pr-details-title">
@ -822,15 +822,17 @@
<table>
<tr><td><code>${_('In pull request description')}:</code></td></tr>
% if c.referenced_desc_issues:
% for ticket_dict in sorted(c.referenced_desc_issues):
% if c.referenced_desc_issues.issues:
% for ticket_id, ticket_dict in c.referenced_desc_issues.unique_issues.items():
<tr>
<td>
<a href="${ticket_dict.get('url')}">
${ticket_dict.get('id')}
<a href="${ticket_dict[0].get('url')}">
${ticket_id}
</a>
</td>
</tr>
% endfor
% else:
<tr>
@ -841,13 +843,14 @@
% endif
<tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
% if c.referenced_commit_issues:
% for ticket_dict in sorted(c.referenced_commit_issues):
% if c.referenced_commit_issues.issues:
% for ticket_id, ticket_dict in c.referenced_commit_issues.unique_issues.items():
<tr>
<td>
<a href="${ticket_dict.get('url')}">
${ticket_dict.get('id')}
<a href="${ticket_dict[0].get('url')}">
${ticket_id}
</a>
- ${_ungettext('in %s commit', 'in %s commits', len(ticket_dict)) % (len(ticket_dict))}
</td>
</tr>
% endfor

View file

@ -95,7 +95,8 @@ $(document).ready(function(){
var fname = selectedReference.raw_id + ext;
var href = pyroutes.url('repo_archivefile', {
'repo_name': templateContext.repo_name,
'fname': fname
'fname': fname,
'with_hash': '1'
});
// set new label
$(this).html(ico + ' {0}{1}'.format(escapeHtml(e.added.text), ext));