release: Merge default into stable for release preparation

This commit is contained in:
Marcin Kuzminski 2020-09-28 12:19:44 +00:00
commit 5aa8df7b67
65 changed files with 8505 additions and 1103 deletions

View file

@ -1,6 +1,5 @@
[bumpversion]
current_version = 4.20.1
current_version = 4.21.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.20.1
[task:updated_translation]
[release]
state = in_progress
version = 4.21.0
[task:generate_js_routes]
[task:updated_trial_license]

View file

@ -147,12 +147,13 @@ Use the following example to configure Nginx as a your web server.
## Special Cache for file store, make sure you enable this intentionally as
## it could bypass upload files permissions
# location /_file_store/download {
# location /_file_store/download/gravatars {
#
# proxy_cache cache_zone;
# # ignore Set-Cookie
# proxy_ignore_headers Set-Cookie;
# proxy_ignore_headers Cookie;
# # ignore cache-control
# proxy_ignore_headers Cache-Control;
#
# proxy_cache_key $host$uri$is_args$args;
# proxy_cache_methods GET;

View file

@ -0,0 +1,18 @@
diff -rup Beaker-1.9.1-orig/beaker/session.py Beaker-1.9.1/beaker/session.py
--- Beaker-1.9.1-orig/beaker/session.py 2020-04-10 10:23:04.000000000 +0200
+++ Beaker-1.9.1/beaker/session.py 2020-04-10 10:23:34.000000000 +0200
@@ -156,6 +156,14 @@ def __init__(self, request, id=None, invalidate_corrupt=False,
if timeout and not save_accessed_time:
raise BeakerException("timeout requires save_accessed_time")
self.timeout = timeout
+ # We want to pass timeout param to redis backend to support expiration of keys
+ # In future, I believe, we can use this param for memcached and mongo as well
+ if self.timeout is not None and self.type == 'ext:redis':
+ # The backend expiration should always be a bit longer (I decied to use 2 minutes) than the
+ # session expiration itself to prevent the case where the backend data expires while
+ # the session is being read (PR#153)
+ self.namespace_args['timeout'] = self.timeout + 60 * 2
+
self.save_atime = save_accessed_time
self.use_cookies = use_cookies
self.cookie_expires = cookie_expires

View file

@ -0,0 +1,26 @@
diff -rup Beaker-1.9.1-orig/beaker/ext/redisnm.py Beaker-1.9.1/beaker/ext/redisnm.py
--- Beaker-1.9.1-orig/beaker/ext/redisnm.py 2018-04-10 10:23:04.000000000 +0200
+++ Beaker-1.9.1/beaker/ext/redisnm.py 2018-04-10 10:23:34.000000000 +0200
@@ -30,9 +30,10 @@ class RedisNamespaceManager(NamespaceManager):
clients = SyncDict()
- def __init__(self, namespace, url, **kw):
+ def __init__(self, namespace, url, timeout=None, **kw):
super(RedisNamespaceManager, self).__init__(namespace)
self.lock_dir = None # Redis uses redis itself for locking.
+ self.timeout = timeout
if redis is None:
raise RuntimeError('redis is not available')
@@ -68,6 +69,8 @@ def has_key(self, key):
def set_value(self, key, value, expiretime=None):
value = pickle.dumps(value)
+ if expiretime is None and self.timeout is not None:
+ expiretime = self.timeout
if expiretime is not None:
self.client.setex(self._format_key(key), int(expiretime), value)
else:

View file

@ -32,6 +32,8 @@ self: super: {
patches = [
./patches/beaker/patch-beaker-lock-func-debug.diff
./patches/beaker/patch-beaker-metadata-reuse.diff
./patches/beaker/patch-beaker-improved-redis.diff
./patches/beaker/patch-beaker-improved-redis-2.diff
];
});

View file

@ -35,6 +35,20 @@ self: super: {
license = [ pkgs.lib.licenses.bsdOriginal ];
};
};
"apispec" = super.buildPythonPackage {
name = "apispec-1.0.0";
doCheck = false;
propagatedBuildInputs = [
self."PyYAML"
];
src = fetchurl {
url = "https://files.pythonhosted.org/packages/67/15/346c04988dd67d36007e28145504c520491930c878b1f484a97b27a8f497/apispec-1.0.0.tar.gz";
sha256 = "1712w1anvqrvadjjpvai84vbaygaxabd3zz5lxihdzwzs4gvi9sp";
};
meta = {
license = [ pkgs.lib.licenses.mit ];
};
};
"appenlight-client" = super.buildPythonPackage {
name = "appenlight-client-0.6.26";
doCheck = false;
@ -236,20 +250,23 @@ self: super: {
};
};
"channelstream" = super.buildPythonPackage {
name = "channelstream-0.5.2";
name = "channelstream-0.6.14";
doCheck = false;
propagatedBuildInputs = [
self."gevent"
self."ws4py"
self."marshmallow"
self."python-dateutil"
self."pyramid"
self."pyramid-jinja2"
self."pyramid-apispec"
self."itsdangerous"
self."requests"
self."six"
];
src = fetchurl {
url = "https://files.pythonhosted.org/packages/2b/31/29a8e085cf5bf97fa88e7b947adabfc581a18a3463adf77fb6dada34a65f/channelstream-0.5.2.tar.gz";
sha256 = "1qbm4xdl5hfkja683x546bncg3rqq8qv79w1m1a1wd48cqqzb6rm";
url = "https://files.pythonhosted.org/packages/d4/2d/86d6757ccd06ce673ee224123471da3d45251d061da7c580bfc259bad853/channelstream-0.6.14.tar.gz";
sha256 = "0qgy5j3rj6c8cslzidh32glhkrhbbdxjc008y69v8a0y3zyaz2d3";
};
meta = {
license = [ pkgs.lib.licenses.bsdOriginal ];
@ -862,11 +879,11 @@ self: super: {
};
};
"itsdangerous" = super.buildPythonPackage {
name = "itsdangerous-0.24";
name = "itsdangerous-1.1.0";
doCheck = false;
src = fetchurl {
url = "https://files.pythonhosted.org/packages/dc/b4/a60bcdba945c00f6d608d8975131ab3f25b22f2bcfe1dab221165194b2d4/itsdangerous-0.24.tar.gz";
sha256 = "06856q6x675ly542ig0plbqcyab6ksfzijlyf1hzhgg3sgwgrcyb";
url = "https://files.pythonhosted.org/packages/68/1a/f27de07a8a304ad5fa817bbe383d1238ac4396da447fa11ed937039fa04b/itsdangerous-1.1.0.tar.gz";
sha256 = "068zpbksq5q2z4dckh2k1zbcq43ay74ylqn77rni797j0wyh66rj";
};
meta = {
license = [ pkgs.lib.licenses.bsdOriginal ];
@ -993,6 +1010,17 @@ self: super: {
license = [ pkgs.lib.licenses.bsdOriginal pkgs.lib.licenses.bsd3 ];
};
};
"marshmallow" = super.buildPythonPackage {
name = "marshmallow-2.18.0";
doCheck = false;
src = fetchurl {
url = "https://files.pythonhosted.org/packages/ad/0b/5799965d1c6d5f608d684e2c0dce8a828e0309a3bfe8327d9418a89f591c/marshmallow-2.18.0.tar.gz";
sha256 = "1g0aafpjn7yaxq06yndy8c7rs9n42adxkqq1ayhlr869pr06d3lm";
};
meta = {
license = [ pkgs.lib.licenses.mit ];
};
};
"mistune" = super.buildPythonPackage {
name = "mistune-0.8.4";
doCheck = false;
@ -1522,6 +1550,20 @@ self: super: {
license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
};
};
"pyramid-apispec" = super.buildPythonPackage {
name = "pyramid-apispec-0.3.2";
doCheck = false;
propagatedBuildInputs = [
self."apispec"
];
src = fetchurl {
url = "https://files.pythonhosted.org/packages/2a/30/1dea5d81ea635449572ba60ec3148310d75ae4530c3c695f54b0991bb8c7/pyramid_apispec-0.3.2.tar.gz";
sha256 = "0ffrcqp9dkykivhfcq0v9lgy6w0qhwl6x78925vfjmayly9r8da0";
};
meta = {
license = [ pkgs.lib.licenses.bsdOriginal ];
};
};
"pyramid-mailer" = super.buildPythonPackage {
name = "pyramid-mailer-0.15.1";
doCheck = false;
@ -1763,6 +1805,17 @@ self: super: {
license = [ pkgs.lib.licenses.bsdOriginal { fullName = "LGPL+BSD"; } { fullName = "GNU Library or Lesser General Public License (LGPL)"; } ];
};
};
"PyYAML" = super.buildPythonPackage {
name = "PyYAML-5.3.1";
doCheck = false;
src = fetchurl {
url = "https://files.pythonhosted.org/packages/64/c2/b80047c7ac2478f9501676c988a5411ed5572f35d1beff9cae07d321512c/PyYAML-5.3.1.tar.gz";
sha256 = "0pb4zvkfxfijkpgd1b86xjsqql97ssf1knbd1v53wkg1qm9cgsmq";
};
meta = {
license = [ pkgs.lib.licenses.mit ];
};
};
"redis" = super.buildPythonPackage {
name = "redis-3.4.1";
doCheck = false;
@ -1819,7 +1872,7 @@ self: super: {
};
};
"rhodecode-enterprise-ce" = super.buildPythonPackage {
name = "rhodecode-enterprise-ce-4.20.1";
name = "rhodecode-enterprise-ce-4.20.0";
buildInputs = [
self."pytest"
self."py"

View file

@ -5,7 +5,7 @@ babel==1.3
beaker==1.9.1
bleach==3.1.3
celery==4.3.0
channelstream==0.5.2
channelstream==0.6.14
click==7.0
colander==1.7.0
# our custom configobj
@ -22,7 +22,7 @@ future==0.14.3
futures==3.0.2
infrae.cache==1.0.1
iso8601==0.1.12
itsdangerous==0.24
itsdangerous==1.1.0
kombu==4.6.6
lxml==4.2.5
mako==1.1.0

View file

@ -18,10 +18,11 @@ jsonschema==2.6.0
pluggy==0.13.1
pyasn1-modules==0.2.6
pyramid-jinja2==2.7
pyramid-apispec==0.3.2
scandir==1.10.0
setproctitle==1.1.10
tempita==0.5.2
testpath==0.4.4
transaction==2.4.0
vine==1.3.0
wcwidth==0.1.9
wcwidth==0.1.9

View file

@ -1 +1 @@
4.20.1
4.21.0

View file

@ -48,7 +48,7 @@ PYRAMID_SETTINGS = {}
EXTENSIONS = {}
__version__ = ('.'.join((str(each) for each in VERSION[:3])))
__dbversion__ = 108 # defines current db version for migrations
__dbversion__ = 109 # defines current db version for migrations
__platform__ = platform.system()
__license__ = 'AGPLv3, and Commercial License'
__author__ = 'RhodeCode GmbH'

View file

@ -170,8 +170,7 @@ def validate_repo_permissions(apiuser, repoid, repo, perms):
"""
if not HasRepoPermissionAnyApi(*perms)(
user=apiuser, repo_name=repo.repo_name):
raise JSONRPCError(
'repository `%s` does not exist' % repoid)
raise JSONRPCError('repository `%s` does not exist' % repoid)
return True

View file

@ -307,8 +307,7 @@ def get_repo_changeset(request, apiuser, repoid, revision,
"""
repo = get_repo_or_error(repoid)
if not has_superadmin_permission(apiuser):
_perms = (
'repository.admin', 'repository.write', 'repository.read',)
_perms = ('repository.admin', 'repository.write', 'repository.read',)
validate_repo_permissions(apiuser, repoid, repo, _perms)
changes_details = Optional.extract(details)
@ -366,8 +365,7 @@ def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
"""
repo = get_repo_or_error(repoid)
if not has_superadmin_permission(apiuser):
_perms = (
'repository.admin', 'repository.write', 'repository.read',)
_perms = ('repository.admin', 'repository.write', 'repository.read',)
validate_repo_permissions(apiuser, repoid, repo, _perms)
changes_details = Optional.extract(details)
@ -1021,7 +1019,8 @@ def update_repo(
include_secrets = False
if not has_superadmin_permission(apiuser):
validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
_perms = ('repository.admin',)
validate_repo_permissions(apiuser, repoid, repo, _perms)
else:
include_secrets = True
@ -1208,8 +1207,7 @@ def fork_repo(request, apiuser, repoid, fork_name,
if not has_superadmin_permission(apiuser):
# check if we have at least read permission for
# this repo that we fork !
_perms = (
'repository.admin', 'repository.write', 'repository.read')
_perms = ('repository.admin', 'repository.write', 'repository.read')
validate_repo_permissions(apiuser, repoid, repo, _perms)
# check if the regular user has at least fork permissions as well
@ -2370,12 +2368,13 @@ def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
}
"""
# Restrict access to this api method to admins only.
# Restrict access to this api method to super-admins, and repo admins only.
repo = get_repo_or_error(repoid)
if not has_superadmin_permission(apiuser):
raise JSONRPCForbidden()
_perms = ('repository.admin',)
validate_repo_permissions(apiuser, repoid, repo, _perms)
try:
repo = get_repo_or_error(repoid)
settings_model = VcsSettingsModel(repo=repo)
settings = settings_model.get_global_settings()
settings.update(settings_model.get_repo_settings())
@ -2414,9 +2413,11 @@ def set_repo_settings(request, apiuser, repoid, settings):
"result": true
}
"""
# Restrict access to this api method to admins only.
# Restrict access to this api method to super-admins, and repo admins only.
repo = get_repo_or_error(repoid)
if not has_superadmin_permission(apiuser):
raise JSONRPCForbidden()
_perms = ('repository.admin',)
validate_repo_permissions(apiuser, repoid, repo, _perms)
if type(settings) is not dict:
raise JSONRPCError('Settings have to be a JSON Object.')

View file

@ -34,7 +34,7 @@ from rhodecode.lib.channelstream import (
get_user_data,
parse_channels_info,
update_history_from_logs,
STATE_PUBLIC_KEYS)
USER_STATE_PUBLIC_KEYS)
from rhodecode.lib.auth import NotAnonymous
@ -86,14 +86,16 @@ class ChannelstreamView(BaseAppView):
'display_name': None,
'display_link': None,
}
user_data['permissions'] = self._rhodecode_user.permissions_safe
#user_data['permissions'] = self._rhodecode_user.permissions_safe
payload = {
'username': user.username,
'user_state': user_data,
'conn_id': str(uuid.uuid4()),
'channels': channels,
'channel_configs': {},
'state_public_keys': STATE_PUBLIC_KEYS,
'state_public_keys': USER_STATE_PUBLIC_KEYS,
'info': {
'exclude_channels': ['broadcast']
}
@ -118,10 +120,13 @@ class ChannelstreamView(BaseAppView):
'Channelstream service at {} is down'.format(channelstream_url))
return HTTPBadGateway()
channel_info = connect_result.get('channels_info')
if not channel_info:
raise HTTPBadRequest()
connect_result['channels'] = channels
connect_result['channels_info'] = parse_channels_info(
connect_result['channels_info'],
include_channel_info=filtered_channels)
channel_info, include_channel_info=filtered_channels)
update_history_from_logs(self.channelstream_config,
filtered_channels, connect_result)
return connect_result
@ -167,10 +172,15 @@ class ChannelstreamView(BaseAppView):
log.exception(
'Channelstream service at {} is down'.format(channelstream_url))
return HTTPBadGateway()
channel_info = connect_result.get('channels_info')
if not channel_info:
raise HTTPBadRequest()
# include_channel_info will limit history only to new channel
# to not overwrite histories on other channels in client
connect_result['channels_info'] = parse_channels_info(
connect_result['channels_info'],
channel_info,
include_channel_info=filtered_channels)
update_history_from_logs(
self.channelstream_config, filtered_channels, connect_result)

View file

@ -43,10 +43,10 @@ def includeme(config):
pattern='/_file_store/upload')
config.add_route(
name='download_file',
pattern='/_file_store/download/{fid}')
pattern='/_file_store/download/{fid:.*}')
config.add_route(
name='download_file_by_token',
pattern='/_file_store/token-download/{_auth_token}/{fid}')
pattern='/_file_store/token-download/{_auth_token}/{fid:.*}')
# Scan module for configuration decorators.
config.scan('.views', ignore='.tests')

View file

@ -20,6 +20,7 @@
import os
import time
import errno
import shutil
import hashlib
@ -32,8 +33,23 @@ from rhodecode.apps.file_store.exceptions import (
METADATA_VER = 'v1'
def safe_make_dirs(dir_path):
if not os.path.exists(dir_path):
try:
os.makedirs(dir_path)
except OSError as e:
if e.errno != errno.EEXIST:
raise
return
class LocalFileStorage(object):
@classmethod
def apply_counter(cls, counter, filename):
name_counted = '%d-%s' % (counter, filename)
return name_counted
@classmethod
def resolve_name(cls, name, directory):
"""
@ -47,17 +63,16 @@ class LocalFileStorage(object):
counter = 0
while True:
name = '%d-%s' % (counter, name)
name_counted = cls.apply_counter(counter, name)
# sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
sub_store = cls._sub_store_from_filename(name)
sub_store = cls._sub_store_from_filename(name_counted)
sub_store_path = os.path.join(directory, sub_store)
if not os.path.exists(sub_store_path):
os.makedirs(sub_store_path)
safe_make_dirs(sub_store_path)
path = os.path.join(sub_store_path, name)
path = os.path.join(sub_store_path, name_counted)
if not os.path.exists(path):
return name, path
return name_counted, path
counter += 1
@classmethod
@ -102,8 +117,13 @@ class LocalFileStorage(object):
:param filename: base name of file
"""
sub_store = self._sub_store_from_filename(filename)
return os.path.join(self.base_path, sub_store, filename)
prefix_dir = ''
if '/' in filename:
prefix_dir, filename = filename.split('/')
sub_store = self._sub_store_from_filename(filename)
else:
sub_store = self._sub_store_from_filename(filename)
return os.path.join(self.base_path, prefix_dir, sub_store, filename)
def delete(self, filename):
"""
@ -123,7 +143,7 @@ class LocalFileStorage(object):
Checks if file exists. Resolves filename's absolute
path based on base_path.
:param filename: base name of file
:param filename: file_uid name of file, e.g 0-f62b2b2d-9708-4079-a071-ec3f958448d4.svg
"""
return os.path.exists(self.store_path(filename))
@ -158,7 +178,7 @@ class LocalFileStorage(object):
return ext in [normalize_ext(x) for x in extensions]
def save_file(self, file_obj, filename, directory=None, extensions=None,
extra_metadata=None, max_filesize=None, **kwargs):
extra_metadata=None, max_filesize=None, randomized_name=True, **kwargs):
"""
Saves a file object to the uploads location.
Returns the resolved filename, i.e. the directory +
@ -169,6 +189,7 @@ class LocalFileStorage(object):
:param directory: relative path of sub-directory
:param extensions: iterable of allowed extensions, if not default
:param max_filesize: maximum size of file that should be allowed
:param randomized_name: generate random generated UID or fixed based on the filename
:param extra_metadata: extra JSON metadata to store next to the file with .meta suffix
"""
@ -183,13 +204,12 @@ class LocalFileStorage(object):
else:
dest_directory = self.base_path
if not os.path.exists(dest_directory):
os.makedirs(dest_directory)
safe_make_dirs(dest_directory)
filename = utils.uid_filename(filename)
uid_filename = utils.uid_filename(filename, randomized=randomized_name)
# resolve also produces special sub-dir for file optimized store
filename, path = self.resolve_name(filename, dest_directory)
filename, path = self.resolve_name(uid_filename, dest_directory)
stored_file_dir = os.path.dirname(path)
file_obj.seek(0)
@ -210,12 +230,13 @@ class LocalFileStorage(object):
file_hash = self.calculate_path_hash(path)
metadata.update(
{"filename": filename,
metadata.update({
"filename": filename,
"size": size,
"time": time.time(),
"sha256": file_hash,
"meta_ver": METADATA_VER})
"meta_ver": METADATA_VER
})
filename_meta = filename + '.meta'
with open(os.path.join(stored_file_dir, filename_meta), "wb") as dest_meta:

View file

@ -20,7 +20,7 @@
import uuid
import StringIO
import pathlib2
@ -52,3 +52,7 @@ def uid_filename(filename, randomized=True):
hash_key = '{}.{}'.format(filename, 'store')
uid = uuid.uuid5(uuid.NAMESPACE_URL, hash_key)
return str(uid) + ext.lower()
def bytes_to_file_obj(bytes_data):
return StringIO.StringIO(bytes_data)

View file

@ -64,7 +64,7 @@ class FileStoreView(BaseAppView):
file_uid, store_path)
raise HTTPNotFound()
db_obj = FileStore().query().filter(FileStore.file_uid == file_uid).scalar()
db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
if not db_obj:
raise HTTPNotFound()

View file

@ -345,6 +345,16 @@ def includeme(config):
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
repo_route=True, repo_accepted_types=['hg', 'git'])
config.add_route(
name='pullrequest_comments',
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
repo_route=True)
config.add_route(
name='pullrequest_todos',
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
repo_route=True)
# Artifacts, (EE feature)
config.add_route(
name='repo_artifacts_list',

View file

@ -485,23 +485,10 @@ class TestRepoCommitCommentsView(TestController):
def assert_comment_links(response, comments, inline_comments):
if comments == 1:
comments_text = "%d General" % comments
else:
comments_text = "%d General" % comments
response.mustcontain(
'<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
response.mustcontain(
'<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
if inline_comments == 1:
inline_comments_text = "%d Inline" % inline_comments
else:
inline_comments_text = "%d Inline" % inline_comments
if comments:
response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
else:
response.mustcontain(comments_text)
if inline_comments:
response.mustcontain(
'id="inline-comments-counter">%s' % inline_comments_text)
else:
response.mustcontain(inline_comments_text)

View file

@ -619,7 +619,12 @@ class ComparePage(AssertResponse):
self.contains_one_anchor(file_id)
diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
assert len(diffblock) == 2
assert len(diffblock[0].cssselect('a[href="#%s"]' % file_id)) == 1
for lnk in diffblock[0].cssselect('a'):
if 'permalink' in lnk.text:
assert '#{}'.format(file_id) in lnk.attrib['href']
break
else:
pytest.fail('Unable to find permalink')
def contains_change_summary(self, files_changed, inserted, deleted):
template = (

View file

@ -150,9 +150,9 @@ class TestPullrequestsView(object):
response = self.app.post(
route_path('pullrequest_create', repo_name=source.repo_name),
[
('source_repo', source.repo_name),
('source_repo', source_repo_name),
('source_ref', source_ref),
('target_repo', target.repo_name),
('target_repo', target_repo_name),
('target_ref', target_ref),
('common_ancestor', commit_ids['initial-commit']),
('pullrequest_title', 'Title'),
@ -1110,16 +1110,17 @@ class TestPullrequestsView(object):
# source has ancestor - change - change-2
backend.pull_heads(source, heads=['change-2'])
target_repo_name = target.repo_name
# update PR
self.app.post(
route_path('pullrequest_update',
repo_name=target.repo_name, pull_request_id=pull_request_id),
repo_name=target_repo_name, pull_request_id=pull_request_id),
params={'update_commits': 'true', 'csrf_token': csrf_token})
response = self.app.get(
route_path('pullrequest_show',
repo_name=target.repo_name,
repo_name=target_repo_name,
pull_request_id=pull_request.pull_request_id))
assert response.status_int == 200
@ -1166,10 +1167,11 @@ class TestPullrequestsView(object):
# source has ancestor - ancestor-new - change-rebased
backend.pull_heads(target, heads=['ancestor-new'])
backend.pull_heads(source, heads=['change-rebased'])
target_repo_name = target.repo_name
# update PR
url = route_path('pullrequest_update',
repo_name=target.repo_name,
repo_name=target_repo_name,
pull_request_id=pull_request_id)
self.app.post(url,
params={'update_commits': 'true', 'csrf_token': csrf_token},
@ -1183,7 +1185,7 @@ class TestPullrequestsView(object):
response = self.app.get(
route_path('pullrequest_show',
repo_name=target.repo_name,
repo_name=target_repo_name,
pull_request_id=pull_request.pull_request_id))
assert response.status_int == 200
response.mustcontain('Pull request updated to')
@ -1232,16 +1234,17 @@ class TestPullrequestsView(object):
vcsrepo = target.scm_instance()
vcsrepo.config.clear_section('hooks')
vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
target_repo_name = target.repo_name
# update PR
url = route_path('pullrequest_update',
repo_name=target.repo_name,
repo_name=target_repo_name,
pull_request_id=pull_request_id)
self.app.post(url,
params={'update_commits': 'true', 'csrf_token': csrf_token},
status=200)
response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
assert response.status_int == 200
response.mustcontain('Pull request updated to')
response.mustcontain('with 0 added, 0 removed commits.')
@ -1280,11 +1283,12 @@ class TestPullrequestsView(object):
# source has ancestor - ancestor-new - change-rebased
backend.pull_heads(target, heads=['ancestor-new'])
backend.pull_heads(source, heads=['change-rebased'])
target_repo_name = target.repo_name
# update PR
self.app.post(
route_path('pullrequest_update',
repo_name=target.repo_name, pull_request_id=pull_request_id),
repo_name=target_repo_name, pull_request_id=pull_request_id),
params={'update_commits': 'true', 'csrf_token': csrf_token},
status=200)
@ -1389,6 +1393,8 @@ class TestPullrequestsView(object):
pull_request = pr_util.create_pull_request(
commits, target_head='old-feature', source_head='new-feature',
revisions=['new-feature'], mergeable=True)
pr_id = pull_request.pull_request_id
target_repo_name = pull_request.target_repo.repo_name
vcs = pr_util.source_repository.scm_instance()
if backend.alias == 'git':
@ -1397,8 +1403,8 @@ class TestPullrequestsView(object):
vcs.strip(pr_util.commit_ids['new-feature'])
url = route_path('pullrequest_update',
repo_name=pull_request.target_repo.repo_name,
pull_request_id=pull_request.pull_request_id)
repo_name=target_repo_name,
pull_request_id=pr_id)
response = self.app.post(url,
params={'update_commits': 'true',
'csrf_token': csrf_token})
@ -1409,8 +1415,8 @@ class TestPullrequestsView(object):
# Make sure that after update, it won't raise 500 errors
response = self.app.get(route_path(
'pullrequest_show',
repo_name=pr_util.target_repository.repo_name,
pull_request_id=pull_request.pull_request_id))
repo_name=target_repo_name,
pull_request_id=pr_id))
assert response.status_int == 200
response.assert_response().element_contains(

View file

@ -18,8 +18,8 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
import collections
from pyramid.httpexceptions import (
HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
@ -34,14 +34,14 @@ from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOv
from rhodecode.lib import diffs, codeblocks
from rhodecode.lib.auth import (
LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
from rhodecode.lib.ext_json import json
from rhodecode.lib.compat import OrderedDict
from rhodecode.lib.diffs import (
cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
get_diff_whitespace_flag)
from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
import rhodecode.lib.helpers as h
from rhodecode.lib.utils2 import safe_unicode, str2bool
from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
from rhodecode.lib.vcs.backends.base import EmptyCommit
from rhodecode.lib.vcs.exceptions import (
RepositoryError, CommitDoesNotExistError)
@ -115,6 +115,7 @@ class RepoCommitsView(RepoAppView):
except Exception:
log.exception("General failure")
raise HTTPNotFound()
single_commit = len(c.commit_ranges) == 1
c.changes = OrderedDict()
c.lines_added = 0
@ -128,23 +129,48 @@ class RepoCommitsView(RepoAppView):
c.inline_comments = []
c.files = []
c.statuses = []
c.comments = []
c.unresolved_comments = []
c.resolved_comments = []
if len(c.commit_ranges) == 1:
# Single commit
if single_commit:
commit = c.commit_ranges[0]
c.comments = CommentsModel().get_comments(
self.db_repo.repo_id,
revision=commit.raw_id)
c.statuses.append(ChangesetStatusModel().get_status(
self.db_repo.repo_id, commit.raw_id))
# comments from PR
statuses = ChangesetStatusModel().get_statuses(
self.db_repo.repo_id, commit.raw_id,
with_revisions=True)
prs = set(st.pull_request for st in statuses
if st.pull_request is not None)
prs = set()
reviewers = list()
reviewers_duplicates = set() # to not have duplicates from multiple votes
for c_status in statuses:
# extract associated pull-requests from votes
if c_status.pull_request:
prs.add(c_status.pull_request)
# extract reviewers
_user_id = c_status.author.user_id
if _user_id not in reviewers_duplicates:
reviewers.append(
StrictAttributeDict({
'user': c_status.author,
# fake attributed for commit, page that we don't have
# but we share the display with PR page
'mandatory': False,
'reasons': [],
'rule_user_group_data': lambda: None
})
)
reviewers_duplicates.add(_user_id)
c.allowed_reviewers = reviewers
# from associated statuses, check the pull requests, and
# show comments from them
for pr in prs:
@ -155,6 +181,37 @@ class RepoCommitsView(RepoAppView):
c.resolved_comments = CommentsModel()\
.get_commit_resolved_todos(commit.raw_id)
c.inline_comments_flat = CommentsModel()\
.get_commit_inline_comments(commit.raw_id)
review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
statuses, reviewers)
c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
for review_obj, member, reasons, mandatory, status in review_statuses:
member_reviewer = h.reviewer_as_json(
member, reasons=reasons, mandatory=mandatory,
user_group=None
)
current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
member_reviewer['review_status'] = current_review_status
member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
member_reviewer['allowed_to_update'] = False
c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
# NOTE(marcink): this uses the same voting logic as in pull-requests
c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
c.commit_broadcast_channel = u'/repo${}$/commit/{}'.format(
c.repo_name,
commit.raw_id
)
diff = None
# Iterate over ranges (default commit view is always one commit)
for commit in c.commit_ranges:
@ -166,8 +223,8 @@ class RepoCommitsView(RepoAppView):
if method == 'show':
inline_comments = CommentsModel().get_inline_comments(
self.db_repo.repo_id, revision=commit.raw_id)
c.inline_cnt = CommentsModel().get_inline_comments_count(
inline_comments)
c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
inline_comments))
c.inline_comments = inline_comments
cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
@ -226,6 +283,7 @@ class RepoCommitsView(RepoAppView):
# sort comments by how they were generated
c.comments = sorted(c.comments, key=lambda x: x.comment_id)
c.at_version_num = None
if len(c.commit_ranges) == 1:
c.commit = c.commit_ranges[0]
@ -395,6 +453,7 @@ class RepoCommitsView(RepoAppView):
}
if comment:
c.co = comment
c.at_version_num = 0
rendered_comment = render(
'rhodecode:templates/changeset/changeset_comment_block.mako',
self._get_template_context(c), self.request)
@ -427,7 +486,6 @@ class RepoCommitsView(RepoAppView):
return ''
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@CSRFRequired()

View file

@ -39,7 +39,7 @@ from rhodecode.lib.ext_json import json
from rhodecode.lib.auth import (
LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
NotAnonymous, CSRFRequired)
from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int
from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
from rhodecode.lib.vcs.exceptions import (
CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
@ -265,6 +265,36 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
return diffset
def register_comments_vars(self, c, pull_request, versions):
comments_model = CommentsModel()
# GENERAL COMMENTS with versions #
q = comments_model._all_general_comments_of_pull_request(pull_request)
q = q.order_by(ChangesetComment.comment_id.asc())
general_comments = q
# pick comments we want to render at current version
c.comment_versions = comments_model.aggregate_comments(
general_comments, versions, c.at_version_num)
# INLINE COMMENTS with versions #
q = comments_model._all_inline_comments_of_pull_request(pull_request)
q = q.order_by(ChangesetComment.comment_id.asc())
inline_comments = q
c.inline_versions = comments_model.aggregate_comments(
inline_comments, versions, c.at_version_num, inline=True)
# Comments inline+general
if c.at_version:
c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
c.comments = c.comment_versions[c.at_version_num]['display']
else:
c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
c.comments = c.comment_versions[c.at_version_num]['until']
return general_comments, inline_comments
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@ -280,6 +310,8 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
pull_request_id = pull_request.pull_request_id
c.state_progressing = pull_request.is_state_changing()
c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
pull_request.target_repo.repo_name, pull_request.pull_request_id)
_new_state = {
'created': PullRequest.STATE_CREATED,
@ -300,22 +332,23 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
from_version = self.request.GET.get('from_version') or version
merge_checks = self.request.GET.get('merge_checks')
c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
force_refresh = str2bool(self.request.GET.get('force_refresh'))
c.range_diff_on = self.request.GET.get('range-diff') == "1"
# fetch global flags of ignore ws or context lines
diff_context = diffs.get_diff_context(self.request)
hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
force_refresh = str2bool(self.request.GET.get('force_refresh'))
(pull_request_latest,
pull_request_at_ver,
pull_request_display_obj,
at_version) = PullRequestModel().get_pr_version(
pull_request_id, version=version)
pr_closed = pull_request_latest.is_closed()
if pr_closed and (version or from_version):
# not allow to browse versions
# not allow to browse versions for closed PR
raise HTTPFound(h.route_path(
'pullrequest_show', repo_name=self.db_repo_name,
pull_request_id=pull_request_id))
@ -323,13 +356,13 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
versions = pull_request_display_obj.versions()
# used to store per-commit range diffs
c.changes = collections.OrderedDict()
c.range_diff_on = self.request.GET.get('range-diff') == "1"
c.at_version = at_version
c.at_version_num = (at_version
if at_version and at_version != 'latest'
if at_version and at_version != PullRequest.LATEST_VER
else None)
c.at_version_pos = ChangesetComment.get_index_from_version(
c.at_version_index = ChangesetComment.get_index_from_version(
c.at_version_num, versions)
(prev_pull_request_latest,
@ -340,9 +373,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
c.from_version = prev_at_version
c.from_version_num = (prev_at_version
if prev_at_version and prev_at_version != 'latest'
if prev_at_version and prev_at_version != PullRequest.LATEST_VER
else None)
c.from_version_pos = ChangesetComment.get_index_from_version(
c.from_version_index = ChangesetComment.get_index_from_version(
c.from_version_num, versions)
# define if we're in COMPARE mode or VIEW at version mode
@ -351,16 +384,21 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
# pull_requests repo_name we opened it against
# ie. target_repo must match
if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
log.warning('Mismatch between the current repo: %s, and target %s',
self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
raise HTTPNotFound()
c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
pull_request_at_ver)
c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
c.pull_request = pull_request_display_obj
c.renderer = pull_request_at_ver.description_renderer or c.renderer
c.pull_request_latest = pull_request_latest
if compare or (at_version and not at_version == 'latest'):
# inject latest version
latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
if compare or (at_version and not at_version == PullRequest.LATEST_VER):
c.allowed_to_change_status = False
c.allowed_to_update = False
c.allowed_to_merge = False
@ -389,12 +427,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
'rules' in pull_request_latest.reviewer_data:
rules = pull_request_latest.reviewer_data['rules'] or {}
try:
c.forbid_adding_reviewers = rules.get(
'forbid_adding_reviewers')
c.forbid_author_to_review = rules.get(
'forbid_author_to_review')
c.forbid_commit_author_to_review = rules.get(
'forbid_commit_author_to_review')
c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
c.forbid_author_to_review = rules.get('forbid_author_to_review')
c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
except Exception:
pass
@ -419,41 +454,34 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
return self._get_template_context(c)
comments_model = CommentsModel()
c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
# reviewers and statuses
c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
# GENERAL COMMENTS with versions #
q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
q = q.order_by(ChangesetComment.comment_id.asc())
general_comments = q
for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
member_reviewer = h.reviewer_as_json(
member, reasons=reasons, mandatory=mandatory,
user_group=review_obj.rule_user_group_data()
)
# pick comments we want to render at current version
c.comment_versions = comments_model.aggregate_comments(
general_comments, versions, c.at_version_num)
c.comments = c.comment_versions[c.at_version_num]['until']
current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
member_reviewer['review_status'] = current_review_status
member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
member_reviewer['allowed_to_update'] = c.allowed_to_update
c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
# INLINE COMMENTS with versions #
q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
q = q.order_by(ChangesetComment.comment_id.asc())
inline_comments = q
c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
c.inline_versions = comments_model.aggregate_comments(
inline_comments, versions, c.at_version_num, inline=True)
general_comments, inline_comments = \
self.register_comments_vars(c, pull_request_latest, versions)
# TODOs
c.unresolved_comments = CommentsModel() \
.get_pull_request_unresolved_todos(pull_request)
.get_pull_request_unresolved_todos(pull_request_latest)
c.resolved_comments = CommentsModel() \
.get_pull_request_resolved_todos(pull_request)
# inject latest version
latest_ver = PullRequest.get_pr_display_object(
pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
.get_pull_request_resolved_todos(pull_request_latest)
# if we use version, then do not show later comments
# than current version
@ -520,8 +548,8 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
# empty version means latest, so we keep this to prevent
# double caching
version_normalized = version or 'latest'
from_version_normalized = from_version or 'latest'
version_normalized = version or PullRequest.LATEST_VER
from_version_normalized = from_version or PullRequest.LATEST_VER
cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
cache_file_path = diff_cache_exist(
@ -613,7 +641,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
diff_limit, file_limit, c.fulldiff,
hide_whitespace_changes, diff_context,
use_ancestor=use_ancestor
)
)
# save cached diff
if caching_enabled:
@ -717,7 +745,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
# current user review statuses for each version
c.review_versions = {}
if self._rhodecode_user.user_id in allowed_reviewers:
if self._rhodecode_user.user_id in c.allowed_reviewers:
for co in general_comments:
if co.author.user_id == self._rhodecode_user.user_id:
status = co.status_change
@ -933,6 +961,90 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
}
return data
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='pullrequest_comments', request_method='POST',
renderer='string', xhr=True)
def pullrequest_comments(self):
self.load_default_context()
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
pull_request_id = pull_request.pull_request_id
version = self.request.GET.get('version')
_render = self.request.get_partial_renderer(
'rhodecode:templates/base/sidebar.mako')
c = _render.get_call_context()
(pull_request_latest,
pull_request_at_ver,
pull_request_display_obj,
at_version) = PullRequestModel().get_pr_version(
pull_request_id, version=version)
versions = pull_request_display_obj.versions()
latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
c.at_version = at_version
c.at_version_num = (at_version
if at_version and at_version != PullRequest.LATEST_VER
else None)
self.register_comments_vars(c, pull_request_latest, versions)
all_comments = c.inline_comments_flat + c.comments
existing_ids = filter(
lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
return _render('comments_table', all_comments, len(all_comments),
existing_ids=existing_ids)
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='pullrequest_todos', request_method='POST',
renderer='string', xhr=True)
def pullrequest_todos(self):
self.load_default_context()
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
pull_request_id = pull_request.pull_request_id
version = self.request.GET.get('version')
_render = self.request.get_partial_renderer(
'rhodecode:templates/base/sidebar.mako')
c = _render.get_call_context()
(pull_request_latest,
pull_request_at_ver,
pull_request_display_obj,
at_version) = PullRequestModel().get_pr_version(
pull_request_id, version=version)
versions = pull_request_display_obj.versions()
latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
c.at_version = at_version
c.at_version_num = (at_version
if at_version and at_version != PullRequest.LATEST_VER
else None)
c.unresolved_comments = CommentsModel() \
.get_pull_request_unresolved_todos(pull_request)
c.resolved_comments = CommentsModel() \
.get_pull_request_resolved_todos(pull_request)
all_comments = c.unresolved_comments + c.resolved_comments
existing_ids = filter(
lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
return _render('comments_table', all_comments, len(c.unresolved_comments),
todo_comments=True, existing_ids=existing_ids)
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
@ -1098,7 +1210,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
self.request.matchdict['pull_request_id'])
_ = self.request.translate
self.load_default_context()
c = self.load_default_context()
redirect_url = None
if pull_request.is_closed():
@ -1109,6 +1221,8 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
'redirect_url': redirect_url}
is_state_changing = pull_request.is_state_changing()
c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
pull_request.target_repo.repo_name, pull_request.pull_request_id)
# only owner or admin can update it
allowed_to_update = PullRequestModel().check_user_update(
@ -1132,7 +1246,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
return {'response': True,
'redirect_url': redirect_url}
self._update_commits(pull_request)
self._update_commits(c, pull_request)
if force_refresh:
redirect_url = h.route_path(
'pullrequest_show', repo_name=self.db_repo_name,
@ -1168,7 +1282,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
h.flash(msg, category='success')
return
def _update_commits(self, pull_request):
def _update_commits(self, c, pull_request):
_ = self.request.translate
with pull_request.set_state(PullRequest.STATE_UPDATING):
@ -1196,13 +1310,18 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
change_source=changed)
h.flash(msg, category='success')
channel = '/repo${}$/pr/{}'.format(
pull_request.target_repo.repo_name, pull_request.pull_request_id)
message = msg + (
' - <a onclick="window.location.reload()">'
'<strong>{}</strong></a>'.format(_('Reload page')))
message_obj = {
'message': message,
'level': 'success',
'topic': '/notifications'
}
channelstream.post_message(
channel, message, self._rhodecode_user.username,
c.pr_broadcast_channel, message_obj, self._rhodecode_user.username,
registry=self.request.registry)
else:
msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
@ -1472,6 +1591,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
}
if comment:
c.co = comment
c.at_version_num = None
rendered_comment = render(
'rhodecode:templates/changeset/changeset_comment_block.mako',
self._get_template_context(c), self.request)

View file

@ -1890,7 +1890,7 @@
"url": "http://spdx.org/licenses/BSD-4-Clause.html"
}
],
"name": "python2.7-channelstream-0.5.2"
"name": "python2.7-channelstream-0.6.14"
},
{
"license": [

View file

@ -53,7 +53,7 @@ from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
from rhodecode.lib.exc_tracking import store_exception
from rhodecode.subscribers import (
scan_repositories_if_enabled, write_js_routes_if_enabled,
write_metadata_if_needed, inject_app_settings)
write_metadata_if_needed, write_usage_data, inject_app_settings)
log = logging.getLogger(__name__)
@ -316,6 +316,8 @@ def includeme(config):
pyramid.events.ApplicationCreated)
config.add_subscriber(write_metadata_if_needed,
pyramid.events.ApplicationCreated)
config.add_subscriber(write_usage_data,
pyramid.events.ApplicationCreated)
config.add_subscriber(write_js_routes_if_enabled,
pyramid.events.ApplicationCreated)

View file

@ -145,7 +145,7 @@ class PullRequestCommentEvent(PullRequestEvent):
status = None
if self.comment.status_change:
status = self.comment.status_change[0].status
status = self.comment.review_status
data.update({
'comment': {
@ -184,7 +184,7 @@ class PullRequestCommentEditEvent(PullRequestEvent):
status = None
if self.comment.status_change:
status = self.comment.status_change[0].status
status = self.comment.review_status
data.update({
'comment': {

View file

@ -37,8 +37,9 @@ log = logging.getLogger(__name__)
LOCK = ReadWriteMutex()
STATE_PUBLIC_KEYS = ['id', 'username', 'first_name', 'last_name',
'icon_link', 'display_name', 'display_link']
USER_STATE_PUBLIC_KEYS = [
'id', 'username', 'first_name', 'last_name',
'icon_link', 'display_name', 'display_link']
class ChannelstreamException(Exception):
@ -64,6 +65,8 @@ def channelstream_request(config, payload, endpoint, raise_exc=True):
'x-channelstream-endpoint': endpoint,
'Content-Type': 'application/json'}
req_url = get_channelstream_server_url(config, endpoint)
log.debug('Sending a channelstream request to endpoint: `%s`', req_url)
response = None
try:
response = requests.post(req_url, data=json.dumps(payload),
@ -76,6 +79,7 @@ def channelstream_request(config, payload, endpoint, raise_exc=True):
log.exception('Exception related to Channelstream happened')
if raise_exc:
raise ChannelstreamConnectionException()
log.debug('Got channelstream response: %s', response)
return response
@ -154,7 +158,7 @@ def parse_channels_info(info_result, include_channel_info=None):
for userinfo in info_result['users']:
user_state_dict[userinfo['user']] = {
k: v for k, v in userinfo['state'].items()
if k in STATE_PUBLIC_KEYS
if k in USER_STATE_PUBLIC_KEYS
}
channels_info = {}
@ -163,10 +167,10 @@ def parse_channels_info(info_result, include_channel_info=None):
if c_name not in include_channel_info:
continue
connected_list = []
for userinfo in c_info['users']:
for username in c_info['users']:
connected_list.append({
'user': userinfo['user'],
'state': user_state_dict[userinfo['user']]
'user': username,
'state': user_state_dict[username]
})
channels_info[c_name] = {'users': connected_list,
'history': c_info['history']}
@ -230,6 +234,14 @@ def get_connection_validators(registry):
def post_message(channel, message, username, registry=None):
message_obj = message
if isinstance(message, basestring):
message_obj = {
'message': message,
'level': 'success',
'topic': '/notifications'
}
if not registry:
registry = get_current_registry()
@ -243,11 +255,7 @@ def post_message(channel, message, username, registry=None):
'user': 'system',
'exclude_users': [username],
'channel': channel,
'message': {
'message': message,
'level': 'success',
'topic': '/notifications'
}
'message': message_obj
}
try:

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
import logging
from sqlalchemy import *
from alembic.migration import MigrationContext
from alembic.operations import Operations
from rhodecode.lib.dbmigrate.versions import _reset_base
from rhodecode.model import meta, init_model_encryption
log = logging.getLogger(__name__)
def upgrade(migrate_engine):
"""
Upgrade operations go here.
Don't create your own engine; bind migrate_engine to your metadata
"""
_reset_base(migrate_engine)
from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db
init_model_encryption(db)
context = MigrationContext.configure(migrate_engine.connect())
op = Operations(context)
table = db.PullRequestReviewers.__table__
with op.batch_alter_table(table.name) as batch_op:
new_column = Column('role', Unicode(255), nullable=True)
batch_op.add_column(new_column)
_fill_reviewers_role(db, op, meta.Session)
def downgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
def fixups(models, _SESSION):
pass
def _fill_reviewers_role(models, op, session):
params = {'role': 'reviewer'}
query = text(
'UPDATE pull_request_reviewers SET role = :role'
).bindparams(**params)
op.execute(query)
session().commit()

View file

@ -90,7 +90,7 @@ from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
from rhodecode.lib.index.search_utils import get_matching_line_offsets
from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
from rhodecode.model.changeset_status import ChangesetStatusModel
from rhodecode.model.db import Permission, User, Repository, UserApiKeys
from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
from rhodecode.model.repo_group import RepoGroupModel
from rhodecode.model.settings import IssueTrackerSettingsModel
@ -810,8 +810,7 @@ import tzlocal
local_timezone = tzlocal.get_localzone()
def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
title = value or format_date(datetime_iso)
def get_timezone(datetime_iso, time_is_local=False):
tzinfo = '+00:00'
# detect if we have a timezone info, otherwise, add it
@ -822,6 +821,12 @@ def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
timezone = force_timezone or local_timezone
offset = timezone.localize(datetime_iso).strftime('%z')
tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
return tzinfo
def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
title = value or format_date(datetime_iso)
tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
return literal(
'<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
@ -1357,20 +1362,76 @@ class InitialsGravatar(object):
return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
def initials_gravatar(email_address, first_name, last_name, size=30):
def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
svg_type = None
if email_address == User.DEFAULT_USER_EMAIL:
svg_type = 'default_user'
klass = InitialsGravatar(email_address, first_name, last_name, size)
return klass.generate_svg(svg_type=svg_type)
if store_on_disk:
from rhodecode.apps.file_store import utils as store_utils
from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
FileOverSizeException
from rhodecode.model.db import Session
image_key = md5_safe(email_address.lower()
+ first_name.lower() + last_name.lower())
storage = store_utils.get_file_storage(request.registry.settings)
filename = '{}.svg'.format(image_key)
subdir = 'gravatars'
# since final name has a counter, we apply the 0
uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
store_uid = os.path.join(subdir, uid)
db_entry = FileStore.get_by_store_uid(store_uid)
if db_entry:
return request.route_path('download_file', fid=store_uid)
img_data = klass.get_img_data(svg_type=svg_type)
img_file = store_utils.bytes_to_file_obj(img_data)
try:
store_uid, metadata = storage.save_file(
img_file, filename, directory=subdir,
extensions=['.svg'], randomized_name=False)
except (FileNotAllowedException, FileOverSizeException):
raise
try:
entry = FileStore.create(
file_uid=store_uid, filename=metadata["filename"],
file_hash=metadata["sha256"], file_size=metadata["size"],
file_display_name=filename,
file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
hidden=True, check_acl=False, user_id=1
)
Session().add(entry)
Session().commit()
log.debug('Stored upload in DB as %s', entry)
except Exception:
raise
return request.route_path('download_file', fid=store_uid)
else:
return klass.generate_svg(svg_type=svg_type)
def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
return safe_str(gravatar_url_tmpl)\
.replace('{email}', email_address) \
.replace('{md5email}', md5_safe(email_address.lower())) \
.replace('{netloc}', request.host) \
.replace('{scheme}', request.scheme) \
.replace('{size}', safe_str(size))
def gravatar_url(email_address, size=30, request=None):
request = get_current_request()
request = request or get_current_request()
_use_gravatar = request.call_context.visual.use_gravatar
_gravatar_url = request.call_context.visual.gravatar_url
_gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
email_address = email_address or User.DEFAULT_USER_EMAIL
if isinstance(email_address, unicode):
@ -1379,21 +1440,15 @@ def gravatar_url(email_address, size=30, request=None):
# empty email or default user
if not email_address or email_address == User.DEFAULT_USER_EMAIL:
return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
if _use_gravatar:
# TODO: Disuse pyramid thread locals. Think about another solution to
# get the host and schema here.
request = get_current_request()
tmpl = safe_str(_gravatar_url)
tmpl = tmpl.replace('{email}', email_address)\
.replace('{md5email}', md5_safe(email_address.lower())) \
.replace('{netloc}', request.host)\
.replace('{scheme}', request.scheme)\
.replace('{size}', safe_str(size))
return tmpl
gravatar_url_tmpl = request.call_context.visual.gravatar_url \
or User.DEFAULT_GRAVATAR_URL
return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
else:
return initials_gravatar(email_address, '', '', size=size)
return initials_gravatar(request, email_address, '', '', size=size)
def breadcrumb_repo_link(repo):
@ -1560,7 +1615,7 @@ def _process_url_func(match_obj, repo_name, uid, entry,
# named regex variables
named_vars.update(match_obj.groupdict())
_url = string.Template(entry['url']).safe_substitute(**named_vars)
desc = string.Template(entry['desc']).safe_substitute(**named_vars)
desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
def quote_cleaner(input_str):
@ -1600,17 +1655,18 @@ def get_active_pattern_entries(repo_name):
pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
allowed_link_formats = [
'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
allowed_formats = ['html', 'rst', 'markdown',
'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
if link_format not in allowed_formats:
if link_format not in allowed_link_formats:
raise ValueError('Link format can be only one of:{} got {}'.format(
allowed_formats, link_format))
allowed_link_formats, link_format))
if active_entries is None:
log.debug('Fetch active patterns for repo: %s', repo_name)
log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
active_entries = get_active_pattern_entries(repo_name)
issues_data = []
@ -1668,7 +1724,8 @@ def process_patterns(text_string, repo_name, link_format='html', active_entries=
return new_text, issues_data
def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
issues_container=None):
"""
Parses given text message and makes proper links.
issues are linked to given issue-server, and rest is a commit link
@ -1691,6 +1748,9 @@ def urlify_commit_message(commit_text, repository=None, active_pattern_entries=N
new_text, issues = process_patterns(new_text, repository or '',
active_entries=active_pattern_entries)
if issues_container is not None:
issues_container.extend(issues)
return literal(new_text)
@ -1731,7 +1791,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):
repo_name=None, active_pattern_entries=None, issues_container=None):
def maybe_convert_relative_links(html_source):
if relative_urls:
@ -1748,6 +1808,8 @@ def render(source, renderer='rst', mentions=False, relative_urls=None,
source, issues = process_patterns(
source, repo_name, link_format='rst',
active_entries=active_pattern_entries)
if issues_container is not None:
issues_container.extend(issues)
return literal(
'<div class="rst-block">%s</div>' %
@ -1760,6 +1822,8 @@ def render(source, renderer='rst', mentions=False, relative_urls=None,
source, issues = process_patterns(
source, repo_name, link_format='markdown',
active_entries=active_pattern_entries)
if issues_container is not None:
issues_container.extend(issues)
return literal(
'<div class="markdown-block">%s</div>' %

View file

@ -139,6 +139,18 @@ def is_vcs_call(environ):
return False
def get_path_elem(route_path):
if not route_path:
return None
cleaned_route_path = route_path.lstrip('/')
if cleaned_route_path:
cleaned_route_path_elems = cleaned_route_path.split('/')
if cleaned_route_path_elems:
return cleaned_route_path_elems[0]
return None
def detect_vcs_request(environ, backends):
checks = {
'hg': (is_hg, SimpleHg),
@ -146,6 +158,17 @@ def detect_vcs_request(environ, backends):
'svn': (is_svn, SimpleSvn),
}
handler = None
# List of path views first chunk we don't do any checks
white_list = [
# e.g /_file_store/download
'_file_store'
]
path_info = environ['PATH_INFO']
if get_path_elem(path_info) in white_list:
log.debug('path `%s` in whitelist, skipping...', path_info)
return handler
if VCS_TYPE_KEY in environ:
raw_type = environ[VCS_TYPE_KEY]

View file

@ -224,7 +224,10 @@ class RedisAuthSessions(BaseAuthSessions):
data = client.get(key)
if data:
json_data = pickle.loads(data)
accessed_time = json_data['_accessed_time']
try:
accessed_time = json_data['_accessed_time']
except KeyError:
accessed_time = 0
if accessed_time < expiry_time:
client.delete(key)
deleted_keys += 1

View file

@ -212,10 +212,10 @@ class ChangesetStatusModel(BaseModel):
# TODO(marcink): with group voting, how does rejected work,
# do we ever get rejected state ?
if approved_votes_count == reviewers_number:
if approved_votes_count and (approved_votes_count == reviewers_number):
return ChangesetStatus.STATUS_APPROVED
if rejected_votes_count == reviewers_number:
if rejected_votes_count and (rejected_votes_count == reviewers_number):
return ChangesetStatus.STATUS_REJECTED
return ChangesetStatus.STATUS_UNDER_REVIEW
@ -354,34 +354,37 @@ class ChangesetStatusModel(BaseModel):
Session().add(new_status)
return new_statuses
def aggregate_votes_by_user(self, commit_statuses, reviewers_data):
commit_statuses_map = collections.defaultdict(list)
for st in commit_statuses:
commit_statuses_map[st.author.username] += [st]
reviewers = []
def version(commit_status):
return commit_status.version
for obj in reviewers_data:
if not obj.user:
continue
statuses = commit_statuses_map.get(obj.user.username, None)
if statuses:
status_groups = itertools.groupby(
sorted(statuses, key=version), version)
statuses = [(x, list(y)[0]) for x, y in status_groups]
reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
return reviewers
def reviewers_statuses(self, pull_request):
_commit_statuses = self.get_statuses(
pull_request.source_repo,
pull_request=pull_request,
with_revisions=True)
commit_statuses = collections.defaultdict(list)
for st in _commit_statuses:
commit_statuses[st.author.username] += [st]
pull_request_reviewers = []
def version(commit_status):
return commit_status.version
for obj in pull_request.reviewers:
if not obj.user:
continue
statuses = commit_statuses.get(obj.user.username, None)
if statuses:
status_groups = itertools.groupby(
sorted(statuses, key=version), version)
statuses = [(x, list(y)[0]) for x, y in status_groups]
pull_request_reviewers.append(
(obj, obj.user, obj.reasons, obj.mandatory, statuses))
return pull_request_reviewers
return self.aggregate_votes_by_user(_commit_statuses, pull_request.reviewers)
def calculated_review_status(self, pull_request, reviewers_statuses=None):
"""

View file

@ -91,8 +91,7 @@ class CommentsModel(BaseModel):
# group by versions, and count until, and display objects
comment_groups = collections.defaultdict(list)
[comment_groups[
_co.pull_request_version_id].append(_co) for _co in comments]
[comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
def yield_comments(pos):
for co in comment_groups[pos]:
@ -229,6 +228,14 @@ class CommentsModel(BaseModel):
return todos
def get_commit_inline_comments(self, commit_id):
inline_comments = Session().query(ChangesetComment) \
.filter(ChangesetComment.line_no != None) \
.filter(ChangesetComment.f_path != None) \
.filter(ChangesetComment.revision == commit_id)
inline_comments = inline_comments.all()
return inline_comments
def _log_audit_action(self, action, action_data, auth_user, comment):
audit_logger.store(
action=action,
@ -456,38 +463,54 @@ class CommentsModel(BaseModel):
else:
action = 'repo.commit.comment.create'
comment_id = comment.comment_id
comment_data = comment.get_api_data()
self._log_audit_action(
action, {'data': comment_data}, auth_user, comment)
msg_url = ''
channel = None
if commit_obj:
msg_url = commit_comment_url
repo_name = repo.repo_name
channel = u'/repo${}$/commit/{}'.format(
repo_name,
commit_obj.raw_id
)
elif pull_request_obj:
msg_url = pr_comment_url
repo_name = pr_target_repo.repo_name
channel = u'/repo${}$/pr/{}'.format(
repo_name,
pull_request_id
pull_request_obj.pull_request_id
)
message = '<strong>{}</strong> {} - ' \
'<a onclick="window.location=\'{}\';' \
'window.location.reload()">' \
'<strong>{}</strong></a>'
message = message.format(
user.username, _('made a comment'), msg_url,
_('Show it now'))
if channel:
username = user.username
message = '<strong>{}</strong> {} #{}, {}'
message = message.format(
username,
_('posted a new comment'),
comment_id,
_('Refresh the page to see new comments.'))
channelstream.post_message(
channel, message, user.username,
registry=get_current_registry())
message_obj = {
'message': message,
'level': 'success',
'topic': '/notifications'
}
channelstream.post_message(
channel, message_obj, user.username,
registry=get_current_registry())
message_obj = {
'message': None,
'user': username,
'comment_id': comment_id,
'topic': '/comment'
}
channelstream.post_message(
channel, message_obj, user.username,
registry=get_current_registry())
return comment
@ -641,16 +664,16 @@ class CommentsModel(BaseModel):
q = self._get_inline_comments_query(repo_id, revision, pull_request)
return self._group_comments_by_path_and_line_number(q)
def get_inline_comments_count(self, inline_comments, skip_outdated=True,
version=None):
inline_cnt = 0
def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
version=None):
inline_comms = []
for fname, per_line_comments in inline_comments.iteritems():
for lno, comments in per_line_comments.iteritems():
for comm in comments:
if not comm.outdated_at_version(version) and skip_outdated:
inline_cnt += 1
inline_comms.append(comm)
return inline_cnt
return inline_comms
def get_outdated_comments(self, repo_id, pull_request):
# TODO: johbo: Remove `repo_id`, it is not needed to find the comments

View file

@ -3809,6 +3809,10 @@ class ChangesetComment(Base, BaseModel):
def outdated(self):
return self.display_state == self.COMMENT_OUTDATED
@property
def outdated_js(self):
return json.dumps(self.display_state == self.COMMENT_OUTDATED)
@property
def immutable(self):
return self.immutable_state == self.OP_IMMUTABLE
@ -3817,16 +3821,35 @@ class ChangesetComment(Base, BaseModel):
"""
Checks if comment is outdated for given pull request version
"""
return self.outdated and self.pull_request_version_id != version
def version_check():
return self.pull_request_version_id and self.pull_request_version_id != version
if self.is_inline:
return self.outdated and version_check()
else:
# general comments don't have .outdated set, also latest don't have a version
return version_check()
def outdated_at_version_js(self, version):
"""
Checks if comment is outdated for given pull request version
"""
return json.dumps(self.outdated_at_version(version))
def older_than_version(self, version):
"""
Checks if comment is made from previous version than given
"""
if version is None:
return self.pull_request_version_id is not None
return self.pull_request_version != version
return self.pull_request_version_id < version
return self.pull_request_version < version
def older_than_version_js(self, version):
"""
Checks if comment is made from previous version than given
"""
return json.dumps(self.older_than_version(version))
@property
def commit_id(self):
@ -3843,7 +3866,9 @@ class ChangesetComment(Base, BaseModel):
@property
def is_inline(self):
return self.line_no and self.f_path
if self.line_no and self.f_path:
return True
return False
@property
def last_version(self):
@ -3856,6 +3881,16 @@ class ChangesetComment(Base, BaseModel):
return self.get_index_from_version(
self.pull_request_version_id, versions)
@property
def review_status(self):
if self.status_change:
return self.status_change[0].status
@property
def review_status_lbl(self):
if self.status_change:
return self.status_change[0].status_lbl
def __repr__(self):
if self.comment_id:
return '<DB:Comment #%s>' % self.comment_id
@ -4133,6 +4168,23 @@ class _PullRequestBase(BaseModel):
def reviewer_data_json(self):
return json.dumps(self.reviewer_data)
@property
def last_merge_metadata_parsed(self):
metadata = {}
if not self.last_merge_metadata:
return metadata
if hasattr(self.last_merge_metadata, 'de_coerce'):
for k, v in self.last_merge_metadata.de_coerce().items():
if k in ['target_ref', 'source_ref']:
metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
else:
if hasattr(v, 'de_coerce'):
metadata[k] = v.de_coerce()
else:
metadata[k] = v
return metadata
@property
def work_in_progress(self):
"""checks if pull request is work in progress by checking the title"""
@ -4306,6 +4358,7 @@ class PullRequest(Base, _PullRequestBase):
__table_args__ = (
base_table_args,
)
LATEST_VER = 'latest'
pull_request_id = Column(
'pull_request_id', Integer(), nullable=False, primary_key=True)
@ -4364,6 +4417,10 @@ class PullRequest(Base, _PullRequestBase):
def pull_request_version_id(self):
return getattr(pull_request_obj, 'pull_request_version_id', None)
@property
def pull_request_last_version(self):
return pull_request_obj.pull_request_last_version
attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
attrs.author = StrictAttributeDict(
@ -4428,6 +4485,10 @@ class PullRequest(Base, _PullRequestBase):
"""
return self.versions.count() + 1
@property
def pull_request_last_version(self):
return self.versions_count
class PullRequestVersion(Base, _PullRequestBase):
__tablename__ = 'pull_request_versions'
@ -4475,6 +4536,8 @@ class PullRequestReviewers(Base, BaseModel):
__table_args__ = (
base_table_args,
)
ROLE_REVIEWER = u'reviewer'
ROLE_OBSERVER = u'observer'
@hybrid_property
def reasons(self):
@ -4502,6 +4565,8 @@ class PullRequestReviewers(Base, BaseModel):
JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
user = relationship('User')
pull_request = relationship('PullRequest')
@ -5425,8 +5490,11 @@ class FileStore(Base, BaseModel):
repo_group = relationship('RepoGroup', lazy='joined')
@classmethod
def get_by_store_uid(cls, file_store_uid):
return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
def get_by_store_uid(cls, file_store_uid, safe=False):
if safe:
return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
else:
return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
@classmethod
def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',

View file

@ -1600,7 +1600,7 @@ class PullRequestModel(BaseModel):
'source_ref': pull_request.source_ref_parts,
}
if pull_request.last_merge_metadata:
metadata.update(pull_request.last_merge_metadata)
metadata.update(pull_request.last_merge_metadata_parsed)
if not possible and target_ref.type == 'branch':
# NOTE(marcink): case for mercurial multiple heads on branch

View file

@ -55,3 +55,16 @@
margin: 0 auto 35px auto;
}
}
.alert-text-success {
color: @alert1;
}
.alert-text-error {
color: @alert2;
}
.alert-text-warning {
color: @alert3;
}

View file

@ -254,7 +254,7 @@ input[type="button"] {
.btn-group-actions {
position: relative;
z-index: 100;
z-index: 50;
&:not(.open) .btn-action-switcher-container {
display: none;

View file

@ -1078,10 +1078,16 @@ input.filediff-collapse-state {
background: @color5;
color: white;
}
&[op="comments"] { /* comments on file */
background: @grey4;
color: white;
}
&[op="options"] { /* context menu */
background: @grey6;
color: black;
}
}
}

View file

@ -31,6 +31,10 @@ a { cursor: pointer; }
clear: both;
}
.display-none {
display: none;
}
.pull-right {
float: right !important;
}

View file

@ -240,14 +240,14 @@ div.markdown-block ol {
div.markdown-block ul.checkbox li,
div.markdown-block ol.checkbox li {
list-style: none !important;
margin: 6px !important;
margin: 0px !important;
padding: 0 !important;
}
div.markdown-block ul li,
div.markdown-block ol li {
list-style: disc !important;
margin: 6px !important;
margin: 0px !important;
padding: 0 !important;
}

View file

@ -83,6 +83,11 @@ body {
}
}
.flex-container {
display: flex;
justify-content: space-between;
}
.action-link{
margin-left: @padding;
padding-left: @padding;
@ -482,10 +487,15 @@ ul.auth_plugins {
text-align: left;
overflow: hidden;
white-space: pre-line;
padding-top: 5px
}
.pr-details-title {
height: 16px
#add_reviewer {
padding-top: 10px;
}
#add_reviewer_input {
padding-top: 10px
}
.pr-details-title-author-pref {
@ -1173,9 +1183,12 @@ label {
a {
color: @grey5
}
@media screen and (max-width: 1200px) {
// 1024px or smaller
@media screen and (max-width: 1180px) {
display: none;
}
}
img {
@ -1492,26 +1505,17 @@ table.integrations {
// Pull Requests
.summary-details {
width: 72%;
width: 100%;
}
.pr-summary {
border-bottom: @border-thickness solid @grey5;
margin-bottom: @space;
}
.reviewers-title {
width: 25%;
min-width: 200px;
&.first-panel {
margin-top: 34px;
}
}
.reviewers {
width: 25%;
min-width: 200px;
width: 98%;
}
.reviewers ul li {
position: relative;
width: 100%;
@ -1523,18 +1527,14 @@ table.integrations {
min-height: 55px;
}
.reviewers_member {
width: 100%;
overflow: auto;
}
.reviewer_reason {
padding-left: 20px;
line-height: 1.5em;
}
.reviewer_status {
display: inline-block;
width: 25px;
min-width: 25px;
width: 20px;
min-width: 20px;
height: 1.2em;
line-height: 1em;
}
@ -1557,25 +1557,20 @@ table.integrations {
}
.reviewer_member_mandatory {
position: absolute;
left: 15px;
top: 8px;
width: 16px;
font-size: 11px;
margin: 0;
padding: 0;
color: black;
opacity: 0.4;
}
.reviewer_member_mandatory_remove,
.reviewer_member_remove {
position: absolute;
right: 0;
top: 0;
width: 16px;
margin-bottom: 10px;
padding: 0;
color: black;
cursor: pointer;
}
.reviewer_member_mandatory_remove {
@ -1593,6 +1588,9 @@ table.integrations {
cursor: pointer;
}
.pr-details-title {
height: 20px;
line-height: 20px;
padding-bottom: 8px;
border-bottom: @border-thickness solid @grey5;
@ -1617,7 +1615,7 @@ table.integrations {
text-decoration: line-through;
}
.todo-table {
.todo-table, .comments-table {
width: 100%;
td {
@ -1627,7 +1625,8 @@ table.integrations {
.td-todo-number {
text-align: left;
white-space: nowrap;
width: 15%;
width: 1%;
padding-right: 2px;
}
.td-todo-gravatar {
@ -1651,10 +1650,13 @@ table.integrations {
text-overflow: ellipsis;
}
table.group_members {
width: 100%
}
.group_members {
margin-top: 0;
padding: 0;
list-style: outside none none;
img {
height: @gravatar-size;
@ -1698,7 +1700,7 @@ table.integrations {
}
.reviewer_ac .ac-input {
width: 92%;
width: 100%;
margin-bottom: 1em;
}
@ -2772,7 +2774,7 @@ table.rctable td.td-search-results div {
}
#help_kb .modal-content{
max-width: 750px;
max-width: 800px;
margin: 10% auto;
table{
@ -3069,4 +3071,141 @@ form.markup-form {
.pr-hovercard-title {
padding-top: 5px;
}
}
.action-divider {
opacity: 0.5;
}
.details-inline-block {
display: inline-block;
position: relative;
}
.details-inline-block summary {
list-style: none;
}
details:not([open]) > :not(summary) {
display: none !important;
}
.details-reset > summary {
list-style: none;
}
.details-reset > summary::-webkit-details-marker {
display: none;
}
.details-dropdown {
position: absolute;
top: 100%;
width: 185px;
list-style: none;
background-color: #fff;
background-clip: padding-box;
border: 1px solid @grey5;
box-shadow: 0 8px 24px rgba(149, 157, 165, .2);
left: -150px;
text-align: left;
z-index: 90;
}
.dropdown-divider {
display: block;
height: 0;
margin: 8px 0;
border-top: 1px solid @grey5;
}
.dropdown-item {
display: block;
padding: 4px 8px 4px 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: normal;
}
.right-sidebar {
position: fixed;
top: 0px;
bottom: 0;
right: 0;
background: #fafafa;
z-index: 50;
}
.right-sidebar {
border-left: 1px solid @grey5;
}
.right-sidebar.right-sidebar-expanded {
width: 300px;
overflow: scroll;
}
.right-sidebar.right-sidebar-collapsed {
width: 40px;
padding: 0;
display: block;
overflow: hidden;
}
.sidenav {
float: right;
will-change: min-height;
background: #fafafa;
width: 100%;
}
.sidebar-toggle {
height: 30px;
text-align: center;
margin: 15px 0px 0 0;
}
.sidebar-toggle a {
}
.sidebar-content {
margin-left: 15px;
margin-right: 15px;
}
.sidebar-heading {
font-size: 1.2em;
font-weight: 700;
margin-top: 10px;
}
.sidebar-element {
margin-top: 20px;
}
.right-sidebar-collapsed-state {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0 10px;
cursor: pointer;
font-size: 1.3em;
margin: 0 -15px;
}
.right-sidebar-collapsed-state:hover {
background-color: @grey5;
}
.old-comments-marker {
text-align: left;
}
.old-comments-marker td {
padding-top: 15px;
border-bottom: 1px solid @grey5;
}

View file

@ -790,7 +790,7 @@ input {
&.main_filter_input {
padding: 5px 10px;
min-width: 340px;
color: @grey7;
background: @black;
min-height: 18px;
@ -800,11 +800,34 @@ input {
color: @grey2 !important;
background: white !important;
}
&:focus {
color: @grey2 !important;
background: white !important;
}
min-width: 360px;
@media screen and (max-width: 1600px) {
min-width: 300px;
}
@media screen and (max-width: 1500px) {
min-width: 280px;
}
@media screen and (max-width: 1400px) {
min-width: 260px;
}
@media screen and (max-width: 1300px) {
min-width: 240px;
}
@media screen and (max-width: 1200px) {
min-width: 220px;
}
@media screen and (max-width: 720px) {
min-width: 140px;
}
}
}

View file

@ -168,6 +168,7 @@
.icon-remove:before { content: '\e810'; } /* '' */
.icon-fork:before { content: '\e811'; } /* '' */
.icon-more:before { content: '\e812'; } /* '' */
.icon-options:before { content: '\e812'; } /* '' */
.icon-search:before { content: '\e813'; } /* '' */
.icon-scissors:before { content: '\e814'; } /* '' */
.icon-download:before { content: '\e815'; } /* '' */
@ -251,6 +252,7 @@
// TRANSFORM
.icon-merge:before {transform: rotate(180deg);}
.icon-wide-mode:before {transform: rotate(90deg);}
.icon-options:before {transform: rotate(90deg);}
// -- END ICON CLASSES -- //

View file

@ -131,6 +131,11 @@ function setRCMouseBindings(repoName, repoLandingRev) {
window.location = pyroutes.url(
'edit_repo_perms', {'repo_name': repoName});
});
Mousetrap.bind(['t s'], function(e) {
if (window.toggleSidebar !== undefined) {
window.toggleSidebar();
}
});
}
}

View file

@ -246,6 +246,8 @@ function registerRCRoutes() {
pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
pyroutes.register('pullrequest_comment_edit', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/edit', ['repo_name', 'pull_request_id', 'comment_id']);
pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);

View file

@ -28,9 +28,12 @@ export class RhodecodeApp extends PolymerElement {
super.connectedCallback();
ccLog.debug('rhodeCodeApp created');
$.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
$.Topic('/comment').subscribe(this.handleComment.bind(this));
$.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
$.Topic('/connection_controller/subscribe').subscribe(
this.subscribeToChannelTopic.bind(this));
this.subscribeToChannelTopic.bind(this)
);
// this event can be used to coordinate plugins to do their
// initialization before channelstream is kicked off
$.Topic('/__MAIN_APP__').publish({});
@ -71,6 +74,14 @@ export class RhodecodeApp extends PolymerElement {
}
handleComment(data) {
if (data.message.comment_id) {
if (window.refreshAllComments !== undefined) {
refreshAllComments()
}
}
}
faviconUpdate(data) {
this.shadowRoot.querySelector('rhodecode-favicon').counter = data.count;
}
@ -95,6 +106,7 @@ export class RhodecodeApp extends PolymerElement {
}
// append any additional channels registered in other plugins
$.Topic('/connection_controller/subscribe').processPrepared();
channelstreamConnection.connect();
}
}
@ -157,8 +169,7 @@ export class RhodecodeApp extends PolymerElement {
handleConnected(event) {
var channelstreamConnection = this.getChannelStreamConnection();
channelstreamConnection.set('channelsState',
event.detail.channels_info);
channelstreamConnection.set('channelsState', event.detail.channels_info);
channelstreamConnection.set('userState', event.detail.state);
channelstreamConnection.set('channels', event.detail.channels);
this.propagageChannelsState();

View file

@ -296,16 +296,25 @@ var tooltipActivate = function () {
// we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
if ($origin.data('loaded') !== true) {
var hovercardUrl = $origin.data('hovercardUrl');
var altHovercard =$origin.data('hovercardAlt');
var altHovercard = $origin.data('hovercardAlt');
if (hovercardUrl !== undefined && hovercardUrl !== "") {
if (hovercardUrl.substr(0,12) === 'pyroutes.url'){
var urlLoad = true;
if (hovercardUrl.substr(0, 12) === 'pyroutes.url') {
hovercardUrl = eval(hovercardUrl)
} else if (hovercardUrl.substr(0, 11) === 'javascript:') {
var jsFunc = hovercardUrl.substr(11);
urlLoad = false;
loaded = true;
instance.content(eval(jsFunc))
}
if (urlLoad) {
var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
instance.content(data);
})
}
var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
instance.content(data);
})
} else {
if ($origin.data('hovercardAltHtml')) {
var data = atob($origin.data('hovercardAltHtml'));
@ -677,7 +686,9 @@ var feedLifetimeOptions = function(query, initialData){
query.callback(data);
};
/*
* Retrievew via templateContext.session_attrs.key
* */
var storeUserSessionAttr = function (key, val) {
var postData = {

View file

@ -558,7 +558,7 @@ var CommentsController = function() {
return false;
};
this.showVersion = function (comment_id, comment_history_id) {
this.showVersion = function (comment_id, comment_history_id) {
var historyViewUrl = pyroutes.url(
'repo_commit_comment_history_view',
@ -585,7 +585,7 @@ var CommentsController = function() {
successRenderCommit,
failRenderCommit
);
};
};
this.getLineNumber = function(node) {
var $node = $(node);
@ -670,8 +670,20 @@ var CommentsController = function() {
var success = function(response) {
$comment.remove();
if (window.updateSticky !== undefined) {
// potentially our comments change the active window size, so we
// notify sticky elements
updateSticky()
}
if (window.refreshAllComments !== undefined) {
// if we have this handler, run it, and refresh all comments boxes
refreshAllComments()
}
return false;
};
var failure = function(jqXHR, textStatus, errorThrown) {
var prefix = "Error while deleting this comment.\n"
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
@ -682,6 +694,9 @@ var CommentsController = function() {
return false;
};
ajaxPOST(url, postData, success, failure);
}
this.deleteComment = function(node) {
@ -727,6 +742,15 @@ var CommentsController = function() {
$filediff.find('.hide-line-comments').removeClass('hide-line-comments');
$filediff.toggleClass('hide-comments');
}
// since we change the height of the diff container that has anchor points for upper
// sticky header, we need to tell it to re-calculate those
if (window.updateSticky !== undefined) {
// potentially our comments change the active window size, so we
// notify sticky elements
updateSticky()
}
return false;
};
@ -747,7 +771,7 @@ var CommentsController = function() {
var cm = commentForm.getCmInstance();
if (resolvesCommentId){
var placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
}
setTimeout(function() {
@ -1077,9 +1101,15 @@ var CommentsController = function() {
updateSticky()
}
if (window.refreshAllComments !== undefined) {
// if we have this handler, run it, and refresh all comments boxes
refreshAllComments()
}
commentForm.setActionButtonsDisabled(false);
};
var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
var prefix = "Error while editing comment.\n"
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
@ -1209,6 +1239,11 @@ var CommentsController = function() {
updateSticky()
}
if (window.refreshAllComments !== undefined) {
// if we have this handler, run it, and refresh all comments boxes
refreshAllComments()
}
commentForm.setActionButtonsDisabled(false);
};

View file

@ -35,4 +35,75 @@ var quick_repo_menu = function() {
}, function() {
hide_quick_repo_menus();
});
};
};
window.toggleElement = function (elem, target) {
var $elem = $(elem);
var $target = $(target);
if ($target.is(':visible') || $target.length === 0) {
$target.hide();
$elem.html($elem.data('toggleOn'))
} else {
$target.show();
$elem.html($elem.data('toggleOff'))
}
return false
}
var marginExpVal = '300' // needs a sync with `.right-sidebar.right-sidebar-expanded` value
var marginColVal = '40' // needs a sync with `.right-sidebar.right-sidebar-collapsed` value
var marginExpanded = {'margin': '0 {0}px 0 0'.format(marginExpVal)};
var marginCollapsed = {'margin': '0 {0}px 0 0'.format(marginColVal)};
var updateStickyHeader = function () {
if (window.updateSticky !== undefined) {
// potentially our comments change the active window size, so we
// notify sticky elements
updateSticky()
}
}
var expandSidebar = function () {
var $sideBar = $('.right-sidebar');
$('.outerwrapper').css(marginExpanded);
$('.sidebar-toggle a').html('<i class="icon-right" style="margin-right: -10px"></i><i class="icon-right"></i>');
$('.right-sidebar-collapsed-state').hide();
$('.right-sidebar-expanded-state').show();
$('.branding').addClass('display-none');
$sideBar.addClass('right-sidebar-expanded')
$sideBar.removeClass('right-sidebar-collapsed')
}
var collapseSidebar = function () {
var $sideBar = $('.right-sidebar');
$('.outerwrapper').css(marginCollapsed);
$('.sidebar-toggle a').html('<i class="icon-left" style="margin-right: -10px"></i><i class="icon-left"></i>');
$('.right-sidebar-collapsed-state').show();
$('.right-sidebar-expanded-state').hide();
$('.branding').removeClass('display-none');
$sideBar.removeClass('right-sidebar-expanded')
$sideBar.addClass('right-sidebar-collapsed')
}
window.toggleSidebar = function () {
var $sideBar = $('.right-sidebar');
if ($sideBar.hasClass('right-sidebar-expanded')) {
// expanded -> collapsed transition
collapseSidebar();
var sidebarState = 'collapsed';
} else {
// collapsed -> expanded
expandSidebar();
var sidebarState = 'expanded';
}
// update our other sticky header in same context
updateStickyHeader();
storeUserSessionAttr('rc_user_session_attr.sidebarState', sidebarState);
}

View file

@ -98,10 +98,13 @@ ReviewersController = function () {
var self = this;
this.$reviewRulesContainer = $('#review_rules');
this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
this.$userRule = $('.pr-user-rule-container');
this.forbidReviewUsers = undefined;
this.$reviewMembers = $('#review_members');
this.currentRequest = null;
this.diffData = null;
this.enabledRules = [];
//dummy handler, we might register our own later
this.diffDataHandler = function(data){};
@ -116,14 +119,17 @@ ReviewersController = function () {
this.hideReviewRules = function () {
self.$reviewRulesContainer.hide();
$(self.$userRule.selector).hide();
};
this.showReviewRules = function () {
self.$reviewRulesContainer.show();
$(self.$userRule.selector).show();
};
this.addRule = function (ruleText) {
self.showReviewRules();
self.enabledRules.push(ruleText);
return '<div>- {0}</div>'.format(ruleText)
};
@ -179,6 +185,7 @@ ReviewersController = function () {
_gettext('Reviewers picked from source code changes.'))
)
}
if (data.rules.forbid_adding_reviewers) {
$('#add_reviewer_input').remove();
self.$rulesList.append(
@ -186,6 +193,7 @@ ReviewersController = function () {
_gettext('Adding new reviewers is forbidden.'))
)
}
if (data.rules.forbid_author_to_review) {
self.forbidReviewUsers.push(data.rules_data.pr_author);
self.$rulesList.append(
@ -193,6 +201,7 @@ ReviewersController = function () {
_gettext('Author is not allowed to be a reviewer.'))
)
}
if (data.rules.forbid_commit_author_to_review) {
if (data.rules_data.forbidden_users) {
@ -208,6 +217,12 @@ ReviewersController = function () {
)
}
// we don't have any rules set, so we inform users about it
if (self.enabledRules.length === 0) {
self.addRule(
_gettext('No review rules set.'))
}
return self.forbidReviewUsers
};
@ -264,8 +279,11 @@ ReviewersController = function () {
$('#user').show(); // show user autocomplete after load
var commitElements = data["diff_info"]['commits'];
if (commitElements.length === 0) {
prButtonLock(true, _gettext('no commits'), 'all');
var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
_gettext('There are no commits to merge.'));
prButtonLock(true, noCommitsMsg, 'all');
} else {
// un-lock PR button, so we cannot send PR before it's calculated
@ -309,7 +327,6 @@ ReviewersController = function () {
};
this.addReviewMember = function (reviewer_obj, reasons, mandatory) {
var members = self.$reviewMembers.get(0);
var id = reviewer_obj.user_id;
var username = reviewer_obj.username;
@ -318,10 +335,10 @@ ReviewersController = function () {
// register IDS to check if we don't have this ID already in
var currentIds = [];
var _els = self.$reviewMembers.find('li').toArray();
for (el in _els) {
currentIds.push(_els[el].id)
}
$.each(self.$reviewMembers.find('.reviewer_entry'), function (index, value) {
currentIds.push($(value).data('reviewerUserId'))
})
var userAllowedReview = function (userId) {
var allowed = true;
@ -339,20 +356,23 @@ ReviewersController = function () {
alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
} else {
// only add if it's not there
var alreadyReviewer = currentIds.indexOf('reviewer_' + id) != -1;
var alreadyReviewer = currentIds.indexOf(id) != -1;
if (alreadyReviewer) {
alert(_gettext('User `{0}` already in reviewers').format(username));
} else {
members.innerHTML += renderTemplate('reviewMemberEntry', {
var reviewerEntry = renderTemplate('reviewMemberEntry', {
'member': reviewer_obj,
'mandatory': mandatory,
'reasons': reasons,
'allowed_to_update': true,
'review_status': 'not_reviewed',
'review_status_label': _gettext('Not Reviewed'),
'reasons': reasons,
'create': true
});
'user_group': reviewer_obj.user_group,
'create': true,
'rule_show': true,
})
$(self.$reviewMembers.selector).append(reviewerEntry);
tooltipActivate();
}
}
@ -476,7 +496,7 @@ var ReviewerAutoComplete = function(inputId) {
};
VersionController = function () {
window.VersionController = function () {
var self = this;
this.$verSource = $('input[name=ver_source]');
this.$verTarget = $('input[name=ver_target]');
@ -596,25 +616,10 @@ VersionController = function () {
return false
};
this.toggleElement = function (elem, target) {
var $elem = $(elem);
var $target = $(target);
if ($target.is(':visible')) {
$target.hide();
$elem.html($elem.data('toggleOn'))
} else {
$target.show();
$elem.html($elem.data('toggleOff'))
}
return false
}
};
UpdatePrController = function () {
window.UpdatePrController = function () {
var self = this;
this.$updateCommits = $('#update_commits');
this.$updateCommitsSwitcher = $('#update_commits_switcher');
@ -656,4 +661,230 @@ UpdatePrController = function () {
templateContext.repo_name,
templateContext.pull_request_data.pull_request_id, force);
};
};
};
/**
* Reviewer display panel
*/
window.ReviewersPanel = {
editButton: null,
closeButton: null,
addButton: null,
removeButtons: null,
reviewRules: null,
setReviewers: null,
setSelectors: function () {
var self = this;
self.editButton = $('#open_edit_reviewers');
self.closeButton =$('#close_edit_reviewers');
self.addButton = $('#add_reviewer');
self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
},
init: function (reviewRules, setReviewers) {
var self = this;
self.setSelectors();
this.reviewRules = reviewRules;
this.setReviewers = setReviewers;
this.editButton.on('click', function (e) {
self.edit();
});
this.closeButton.on('click', function (e) {
self.close();
self.renderReviewers();
});
self.renderReviewers();
},
renderReviewers: function () {
$('#review_members').html('')
$.each(this.setReviewers.reviewers, function (key, val) {
var member = val;
var entry = renderTemplate('reviewMemberEntry', {
'member': member,
'mandatory': member.mandatory,
'reasons': member.reasons,
'allowed_to_update': member.allowed_to_update,
'review_status': member.review_status,
'review_status_label': member.review_status_label,
'user_group': member.user_group,
'create': false
});
$('#review_members').append(entry)
});
tooltipActivate();
},
edit: function (event) {
this.editButton.hide();
this.closeButton.show();
this.addButton.show();
$(this.removeButtons.selector).css('visibility', 'visible');
// review rules
reviewersController.loadReviewRules(this.reviewRules);
},
close: function (event) {
this.editButton.show();
this.closeButton.hide();
this.addButton.hide();
$(this.removeButtons.selector).css('visibility', 'hidden');
// hide review rules
reviewersController.hideReviewRules()
}
};
/**
* OnLine presence using channelstream
*/
window.ReviewerPresenceController = function (channel) {
var self = this;
this.channel = channel;
this.users = {};
this.storeUsers = function (users) {
self.users = {}
$.each(users, function (index, value) {
var userId = value.state.id;
self.users[userId] = value.state;
})
}
this.render = function () {
$.each($('.reviewer_entry'), function (index, value) {
var userData = $(value).data();
if (self.users[userData.reviewerUserId] !== undefined) {
$(value).find('.presence-state').show();
} else {
$(value).find('.presence-state').hide();
}
})
};
this.handlePresence = function (data) {
if (data.type == 'presence' && data.channel === self.channel) {
this.storeUsers(data.users);
this.render()
}
};
this.handleChannelUpdate = function (data) {
if (data.channel === this.channel) {
this.storeUsers(data.state.users);
this.render()
}
};
/* subscribe to the current presence */
$.Topic('/connection_controller/presence').subscribe(this.handlePresence.bind(this));
/* subscribe to updates e.g connect/disconnect */
$.Topic('/connection_controller/channel_update').subscribe(this.handleChannelUpdate.bind(this));
};
window.refreshComments = function (version) {
version = version || templateContext.pull_request_data.pull_request_version || '';
// Pull request case
if (templateContext.pull_request_data.pull_request_id !== null) {
var params = {
'pull_request_id': templateContext.pull_request_data.pull_request_id,
'repo_name': templateContext.repo_name,
'version': version,
};
var loadUrl = pyroutes.url('pullrequest_comments', params);
} // commit case
else {
return
}
var currentIDs = []
$.each($('.comment'), function (idx, element) {
currentIDs.push($(element).data('commentId'));
});
var data = {"comments[]": currentIDs};
var $targetElem = $('.comments-content-table');
$targetElem.css('opacity', 0.3);
$targetElem.load(
loadUrl, data, function (responseText, textStatus, jqXHR) {
if (jqXHR.status !== 200) {
return false;
}
var $counterElem = $('#comments-count');
var newCount = $(responseText).data('counter');
if (newCount !== undefined) {
var callback = function () {
$counterElem.animate({'opacity': 1.00}, 200)
$counterElem.html(newCount);
};
$counterElem.animate({'opacity': 0.15}, 200, callback);
}
$targetElem.css('opacity', 1);
tooltipActivate();
}
);
}
window.refreshTODOs = function (version) {
version = version || templateContext.pull_request_data.pull_request_version || '';
// Pull request case
if (templateContext.pull_request_data.pull_request_id !== null) {
var params = {
'pull_request_id': templateContext.pull_request_data.pull_request_id,
'repo_name': templateContext.repo_name,
'version': version,
};
var loadUrl = pyroutes.url('pullrequest_comments', params);
} // commit case
else {
return
}
var currentIDs = []
$.each($('.comment'), function (idx, element) {
currentIDs.push($(element).data('commentId'));
});
var data = {"comments[]": currentIDs};
var $targetElem = $('.todos-content-table');
$targetElem.css('opacity', 0.3);
$targetElem.load(
loadUrl, data, function (responseText, textStatus, jqXHR) {
if (jqXHR.status !== 200) {
return false;
}
var $counterElem = $('#todos-count')
var newCount = $(responseText).data('counter');
if (newCount !== undefined) {
var callback = function () {
$counterElem.animate({'opacity': 1.00}, 200)
$counterElem.html(newCount);
};
$counterElem.animate({'opacity': 0.15}, 200, callback);
}
$targetElem.css('opacity', 1);
tooltipActivate();
}
);
}
window.refreshAllComments = function (version) {
version = version || templateContext.pull_request_data.pull_request_version || '';
refreshComments(version);
refreshTODOs(version);
};

View file

@ -18,6 +18,7 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import io
import math
import re
import os
import datetime
@ -196,6 +197,72 @@ def write_metadata_if_needed(event):
pass
def write_usage_data(event):
import rhodecode
from rhodecode.lib import system_info
from rhodecode.lib import ext_json
settings = event.app.registry.settings
instance_tag = settings.get('metadata.write_usage_tag')
if not settings.get('metadata.write_usage'):
return
def get_update_age(dest_file):
now = datetime.datetime.utcnow()
with open(dest_file, 'rb') as f:
data = ext_json.json.loads(f.read())
if 'created_on' in data:
update_date = parse(data['created_on'])
diff = now - update_date
return math.ceil(diff.total_seconds() / 60.0)
return 0
utc_date = datetime.datetime.utcnow()
hour_quarter = int(math.ceil((utc_date.hour + utc_date.minute/60.0) / 6.))
fname = '.rc_usage_{date.year}{date.month:02d}{date.day:02d}_{hour}.json'.format(
date=utc_date, hour=hour_quarter)
ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
usage_dir = os.path.join(ini_loc, '.rcusage')
if not os.path.isdir(usage_dir):
os.makedirs(usage_dir)
usage_metadata_destination = os.path.join(usage_dir, fname)
try:
age_in_min = get_update_age(usage_metadata_destination)
except Exception:
age_in_min = 0
# write every 6th hour
if age_in_min and age_in_min < 60 * 6:
log.debug('Usage file created %s minutes ago, skipping (threashold: %s)...',
age_in_min, 60 * 6)
return
def write(dest_file):
configuration = system_info.SysInfo(system_info.rhodecode_config)()['value']
license_token = configuration['config']['license_token']
metadata = dict(
desc='Usage data',
instance_tag=instance_tag,
license_token=license_token,
created_on=datetime.datetime.utcnow().isoformat(),
usage=system_info.SysInfo(system_info.usage_info)()['value'],
)
with open(dest_file, 'wb') as f:
f.write(ext_json.json.dumps(metadata, indent=2, sort_keys=True))
try:
log.debug('Writing usage file at: %s', usage_metadata_destination)
write(usage_metadata_destination)
except Exception:
pass
def write_js_routes_if_enabled(event):
registry = event.app.registry

View file

@ -38,10 +38,12 @@
<div class="main">
${next.main()}
</div>
</div>
<!-- END CONTENT -->
</div>
<!-- FOOTER -->
<div id="footer">
<div id="footer-inner" class="title wrapper">
@ -699,9 +701,6 @@
notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
notice_display = 'none' if len(notice_messages) == 0 else ''
%>
<style>
</style>
<ul id="quick" class="main_nav navigation horizontal-list">
## notice box for important system messages
@ -1200,6 +1199,7 @@
('g p', 'Goto pull requests page'),
('g o', 'Goto repository settings'),
('g O', 'Goto repository access permissions settings'),
('t s', 'Toggle sidebar on some pages'),
]
%>
%for key, desc in elems:
@ -1219,3 +1219,36 @@
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<script type="text/javascript">
(function () {
"use sctrict";
var $sideBar = $('.right-sidebar');
var expanded = $sideBar.hasClass('right-sidebar-expanded');
var sidebarState = templateContext.session_attrs.sidebarState;
var sidebarEnabled = $('aside.right-sidebar').get(0);
if (sidebarState === 'expanded') {
expanded = true
} else if (sidebarState === 'collapsed') {
expanded = false
}
if (sidebarEnabled) {
// show sidebar since it's hidden on load
$('.right-sidebar').show();
// init based on set initial class, or if defined user session attrs
if (expanded) {
window.expandSidebar();
window.updateStickyHeader();
} else {
window.collapseSidebar();
window.updateStickyHeader();
}
}
})()
</script>

View file

@ -0,0 +1,142 @@
## snippet for sidebar elements
## usage:
## <%namespace name="sidebar" file="/base/sidebar.mako"/>
## ${sidebar.comments_table()}
<%namespace name="base" file="/base/base.mako"/>
<%def name="comments_table(comments, counter_num, todo_comments=False, existing_ids=None, is_pr=True)">
<%
if todo_comments:
cls_ = 'todos-content-table'
def sorter(entry):
user_id = entry.author.user_id
resolved = '1' if entry.resolved else '0'
if user_id == c.rhodecode_user.user_id:
# own comments first
user_id = 0
return '{}'.format(str(entry.comment_id).zfill(10000))
else:
cls_ = 'comments-content-table'
def sorter(entry):
user_id = entry.author.user_id
return '{}'.format(str(entry.comment_id).zfill(10000))
existing_ids = existing_ids or []
%>
<table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
% for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
<%
display = ''
_cls = ''
%>
<%
comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
prev_comment_ver_index = 0
if loop_obj.previous:
prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
ver_info = None
if getattr(c, 'versions', []):
ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
%>
<% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
<% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
<%
if (prev_comment_ver_index > comment_ver_index):
comments_ver_divider = comment_ver_index
else:
comments_ver_divider = None
%>
% if todo_comments:
% if comment_obj.resolved:
<% _cls = 'resolved-todo' %>
<% display = 'none' %>
% endif
% else:
## SKIP TODOs we display them in other area
% if comment_obj.is_todo:
<% display = 'none' %>
% endif
## Skip outdated comments
% if comment_obj.outdated:
<% display = 'none' %>
<% _cls = 'hidden-comment' %>
% endif
% endif
% if not todo_comments and comments_ver_divider:
<tr class="old-comments-marker">
<td colspan="3">
% if ver_info:
<code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
% else:
<code>v${comments_ver_divider}</code>
% endif
</td>
</tr>
% endif
<tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
<td class="td-todo-number">
<%
version_info = ''
if is_pr:
version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
%>
<script type="text/javascript">
// closure function helper
var sidebarComment${comment_obj.comment_id} = function() {
return renderTemplate('sideBarCommentHovercard', {
version_info: "${version_info}",
file_name: "${comment_obj.f_path}",
line_no: "${comment_obj.line_no}",
outdated: ${h.json.dumps(comment_obj.outdated)},
inline: ${h.json.dumps(comment_obj.is_inline)},
is_todo: ${h.json.dumps(comment_obj.is_todo)},
created_on: "${h.format_date(comment_obj.created_on)}",
datetime: "${comment_obj.created_on}${h.get_timezone(comment_obj.created_on, time_is_local=True)}",
review_status: "${(comment_obj.review_status or '')}"
})
}
</script>
% if comment_obj.outdated:
<i class="icon-comment-toggle tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
% elif comment_obj.is_inline:
<i class="icon-code tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
% else:
<i class="icon-comment tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
% endif
## NEW, since refresh
% if existing_ids and comment_obj.comment_id not in existing_ids:
<span class="tag">NEW</span>
% endif
</td>
<td class="td-todo-gravatar">
${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
</td>
<td class="todo-comment-text-wrapper">
<div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}">
<a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
href="#comment-${comment_obj.comment_id}"
onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}
</a>
</div>
</td>
</tr>
% endfor
</table>
</%def>

View file

@ -4,6 +4,8 @@
<%namespace name="base" file="/base/base.mako"/>
<%namespace name="diff_block" file="/changeset/diff_block.mako"/>
<%namespace name="file_base" file="/files/base.mako"/>
<%namespace name="sidebar" file="/base/sidebar.mako"/>
<%def name="title()">
${_('{} Commit').format(c.repo_name)} - ${h.show_id(c.commit)}
@ -100,22 +102,6 @@
% endif
</div>
%if c.statuses:
<div class="tag status-tag-${c.statuses[0]} pull-right">
<i class="icon-circle review-status-${c.statuses[0]}"></i>
<div class="pull-right">${h.commit_status_lbl(c.statuses[0])}</div>
</div>
%endif
</div>
</div>
</div>
<div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
<div class="left-label-summary">
<p>${_('Commit navigation')}:</p>
<div class="right-label-summary">
<span id="parent_link" class="tag tagtag">
<a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
</span>
@ -123,7 +109,9 @@
<span id="child_link" class="tag tagtag">
<a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
</span>
</div>
</div>
</div>
@ -160,7 +148,9 @@
<%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id], commit=c.commit)}
${cbdiffs.render_diffset(
c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,inline_comments=c.inline_comments )}
c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,
inline_comments=c.inline_comments,
show_todos=False)}
</div>
## template for inline comment form
@ -169,7 +159,7 @@
## comments heading with count
<div class="comments-heading">
<i class="icon-comment"></i>
${_('Comments')} ${len(c.comments)}
${_('General Comments')} ${len(c.comments)}
</div>
## render comments
@ -180,123 +170,262 @@
h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
</div>
## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
<script type="text/javascript">
### NAV SIDEBAR
<aside class="right-sidebar right-sidebar-expanded" id="commit-nav-sticky" style="display: none">
<div class="sidenav navbar__inner" >
## TOGGLE
<div class="sidebar-toggle" onclick="toggleSidebar(); return false">
<a href="#toggleSidebar" class="grey-link-action">
$(document).ready(function() {
</a>
</div>
var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
if($('#trimmed_message_box').height() === boxmax){
$('#message_expand').show();
}
## CONTENT
<div class="sidebar-content">
$('#message_expand').on('click', function(e){
$('#trimmed_message_box').css('max-height', 'none');
$(this).hide();
});
## RULES SUMMARY/RULES
<div class="sidebar-element clear-both">
<% vote_title = _ungettext(
'Status calculated based on votes from {} reviewer',
'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
%>
$('.show-inline-comments').on('click', function(e){
var boxid = $(this).attr('data-comment-id');
var button = $(this);
<div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
<i class="icon-circle review-status-${c.commit_review_status}"></i>
${len(c.allowed_reviewers)}
</div>
</div>
if(button.hasClass("comments-visible")) {
$('#{0} .inline-comments'.format(boxid)).each(function(index){
$(this).hide();
## REVIEWERS
<div class="right-sidebar-expanded-state pr-details-title">
<span class="tooltip sidebar-heading" title="${vote_title}">
<i class="icon-circle review-status-${c.commit_review_status}"></i>
${_('Reviewers')}
</span>
</div>
<div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
<table id="review_members" class="group_members">
## This content is loaded via JS and ReviewersPanel
</table>
</div>
## TODOs
<div class="sidebar-element clear-both">
<div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
<i class="icon-flag-filled"></i>
<span id="todos-count">${len(c.unresolved_comments)}</span>
</div>
<div class="right-sidebar-expanded-state pr-details-title">
## Only show unresolved, that is only what matters
<span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
<i class="icon-flag-filled"></i>
TODOs
</span>
% if c.resolved_comments:
<span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
% else:
<span class="block-right last-item noselect">Show resolved</span>
% endif
</div>
<div class="right-sidebar-expanded-state pr-details-content">
% if c.unresolved_comments + c.resolved_comments:
${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True, is_pr=False)}
% else:
<table>
<tr>
<td>
${_('No TODOs yet')}
</td>
</tr>
</table>
% endif
</div>
</div>
## COMMENTS
<div class="sidebar-element clear-both">
<div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
<i class="icon-comment" style="color: #949494"></i>
<span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
<span class="display-none" id="general-comments-count">${len(c.comments)}</span>
<span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
</div>
<div class="right-sidebar-expanded-state pr-details-title">
<span class="sidebar-heading noselect" onclick="refreshComments(); return false">
<i class="icon-comment" style="color: #949494"></i>
${_('Comments')}
</span>
</div>
<div class="right-sidebar-expanded-state pr-details-content">
% if c.inline_comments_flat + c.comments:
${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments), is_pr=False)}
% else:
<table>
<tr>
<td>
${_('No Comments yet')}
</td>
</tr>
</table>
% endif
</div>
</div>
</div>
</div>
</aside>
## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
<script type="text/javascript">
window.setReviewersData = ${c.commit_set_reviewers_data_json | n};
$(document).ready(function () {
var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
if ($('#trimmed_message_box').height() === boxmax) {
$('#message_expand').show();
}
$('#message_expand').on('click', function (e) {
$('#trimmed_message_box').css('max-height', 'none');
$(this).hide();
});
$('.show-inline-comments').on('click', function (e) {
var boxid = $(this).attr('data-comment-id');
var button = $(this);
if (button.hasClass("comments-visible")) {
$('#{0} .inline-comments'.format(boxid)).each(function (index) {
$(this).hide();
});
button.removeClass("comments-visible");
} else {
$('#{0} .inline-comments'.format(boxid)).each(function(index){
$(this).show();
} else {
$('#{0} .inline-comments'.format(boxid)).each(function (index) {
$(this).show();
});
button.addClass("comments-visible");
}
});
}
});
// next links
$('#child_link').on('click', function(e){
// fetch via ajax what is going to be the next link, if we have
// >1 links show them to user to choose
if(!$('#child_link').hasClass('disabled')){
$.ajax({
// next links
$('#child_link').on('click', function (e) {
// fetch via ajax what is going to be the next link, if we have
// >1 links show them to user to choose
if (!$('#child_link').hasClass('disabled')) {
$.ajax({
url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
success: function(data) {
if(data.results.length === 0){
$('#child_link').html("${_('No Child Commits')}").addClass('disabled');
}
if(data.results.length === 1){
var commit = data.results[0];
window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
}
else if(data.results.length === 2){
$('#child_link').addClass('disabled');
$('#child_link').addClass('double');
success: function (data) {
if (data.results.length === 0) {
$('#child_link').html("${_('No Child Commits')}").addClass('disabled');
}
if (data.results.length === 1) {
var commit = data.results[0];
window.location = pyroutes.url('repo_commit', {
'repo_name': '${c.repo_name}',
'commit_id': commit.raw_id
});
} else if (data.results.length === 2) {
$('#child_link').addClass('disabled');
$('#child_link').addClass('double');
var _html = '';
_html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
.replace('__branch__', data.results[0].branch)
.replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
.replace('__title__', data.results[0].message)
.replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
_html +=' | ';
_html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
.replace('__branch__', data.results[1].branch)
.replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
.replace('__title__', data.results[1].message)
.replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
$('#child_link').html(_html);
}
var _html = '';
_html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
.replace('__branch__', data.results[0].branch)
.replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
.replace('__title__', data.results[0].message)
.replace('__url__', pyroutes.url('repo_commit', {
'repo_name': '${c.repo_name}',
'commit_id': data.results[0].raw_id
}));
_html += ' | ';
_html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
.replace('__branch__', data.results[1].branch)
.replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
.replace('__title__', data.results[1].message)
.replace('__url__', pyroutes.url('repo_commit', {
'repo_name': '${c.repo_name}',
'commit_id': data.results[1].raw_id
}));
$('#child_link').html(_html);
}
}
});
e.preventDefault();
}
});
});
e.preventDefault();
}
});
// prev links
$('#parent_link').on('click', function(e){
// fetch via ajax what is going to be the next link, if we have
// >1 links show them to user to choose
if(!$('#parent_link').hasClass('disabled')){
$.ajax({
// prev links
$('#parent_link').on('click', function (e) {
// fetch via ajax what is going to be the next link, if we have
// >1 links show them to user to choose
if (!$('#parent_link').hasClass('disabled')) {
$.ajax({
url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
success: function(data) {
if(data.results.length === 0){
$('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
}
if(data.results.length === 1){
var commit = data.results[0];
window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
}
else if(data.results.length === 2){
$('#parent_link').addClass('disabled');
$('#parent_link').addClass('double');
success: function (data) {
if (data.results.length === 0) {
$('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
}
if (data.results.length === 1) {
var commit = data.results[0];
window.location = pyroutes.url('repo_commit', {
'repo_name': '${c.repo_name}',
'commit_id': commit.raw_id
});
} else if (data.results.length === 2) {
$('#parent_link').addClass('disabled');
$('#parent_link').addClass('double');
var _html = '';
_html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
.replace('__branch__', data.results[0].branch)
.replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
.replace('__title__', data.results[0].message)
.replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
_html +=' | ';
_html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
.replace('__branch__', data.results[1].branch)
.replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
.replace('__title__', data.results[1].message)
.replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
$('#parent_link').html(_html);
}
var _html = '';
_html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
.replace('__branch__', data.results[0].branch)
.replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
.replace('__title__', data.results[0].message)
.replace('__url__', pyroutes.url('repo_commit', {
'repo_name': '${c.repo_name}',
'commit_id': data.results[0].raw_id
}));
_html += ' | ';
_html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
.replace('__branch__', data.results[1].branch)
.replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
.replace('__title__', data.results[1].message)
.replace('__url__', pyroutes.url('repo_commit', {
'repo_name': '${c.repo_name}',
'commit_id': data.results[1].raw_id
}));
$('#parent_link').html(_html);
}
}
});
e.preventDefault();
}
});
});
e.preventDefault();
}
});
// browse tree @ revision
$('#files_link').on('click', function(e){
window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
e.preventDefault();
});
// browse tree @ revision
$('#files_link').on('click', function (e) {
window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
e.preventDefault();
});
})
</script>
ReviewersPanel.init(null, setReviewersData);
var channel = '${c.commit_broadcast_channel}';
new ReviewerPresenceController(channel)
})
</script>
</%def>

View file

@ -10,12 +10,18 @@
<%namespace name="base" file="/base/base.mako"/>
<%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
<% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
<%
from rhodecode.model.comment import CommentsModel
comment_model = CommentsModel()
%>
<% comment_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
<% latest_ver = len(getattr(c, 'versions', [])) %>
% if inline:
<% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
<% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
% else:
<% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
<% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
% endif
<div class="comment
@ -70,9 +76,9 @@
status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
%>
<i class="icon-circle review-status-${comment.status_change[0].status}"></i>
<i class="icon-circle review-status-${comment.review_status}"></i>
<div class="changeset-status-lbl tooltip" title="${status_change_title}">
${comment.status_change[0].status_lbl}
${comment.review_status_lbl}
</div>
% else:
<div>
@ -153,69 +159,90 @@
</div>
%endif
<a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
<div class="comment-links-block">
% if inline:
<a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
% if outdated_at_ver:
<code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
outdated ${'v{}'.format(pr_index_ver)} |
</code>
% elif pr_index_ver:
<code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
${'v{}'.format(pr_index_ver)} |
</code>
<code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">outdated ${'v{}'.format(comment_ver)}</code>
<code class="action-divider">|</code>
% elif comment_ver:
<code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
<code class="action-divider">|</code>
% endif
</a>
% else:
% if pr_index_ver:
% if comment_ver:
% if comment.outdated:
<a class="pr-version"
href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
>
${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
</a> |
${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
</a>
<code class="action-divider">|</code>
% else:
<a class="tooltip pr-version"
title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}"
title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
>
<code class="pr-version-num">
${'v{}'.format(pr_index_ver)}
</code>
</a> |
<code class="pr-version-num">${'v{}'.format(comment_ver)}</code>
</a>
<code class="action-divider">|</code>
% endif
% endif
% endif
## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
%if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
## permissions to delete
%if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
<a onclick="return Rhodecode.comments.editComment(this);"
class="edit-comment">${_('Edit')}</a>
| <a onclick="return Rhodecode.comments.deleteComment(this);"
class="delete-comment">${_('Delete')}</a>
%else:
<a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
| <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
%endif
%else:
<a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
| <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
%endif
<details class="details-reset details-inline-block">
<summary class="noselect"><i class="icon-options cursor-pointer"></i></summary>
<details-menu class="details-dropdown">
<div class="dropdown-item">
${_('Comment')} #${comment.comment_id}
<span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${comment_model.get_url(comment,request, permalink=True, anchor='comment-{}'.format(comment.comment_id))}" title="${_('Copy permalink')}"></span>
</div>
## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
%if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
## permissions to delete
%if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
<div class="dropdown-divider"></div>
<div class="dropdown-item">
<a onclick="return Rhodecode.comments.editComment(this);" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
</div>
<div class="dropdown-item">
<a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
</div>
%else:
<div class="dropdown-divider"></div>
<div class="dropdown-item">
<a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
</div>
<div class="dropdown-item">
<a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
</div>
%endif
%else:
<div class="dropdown-divider"></div>
<div class="dropdown-item">
<a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
</div>
<div class="dropdown-item">
<a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
</div>
%endif
</details-menu>
</details>
<code class="action-divider">|</code>
% if outdated_at_ver:
| <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
| <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
<a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
<a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
% else:
| <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
| <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
<a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
<a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
% endif
</div>

View file

@ -102,6 +102,11 @@
<%namespace name="diff_block" file="/changeset/diff_block.mako"/>
%for commit in c.commit_ranges:
## commit range header for each individual diff
<h3>
<a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a>
</h3>
${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
${cbdiffs.render_diffset(
diffset=c.changes[commit.raw_id],

View file

@ -61,6 +61,8 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
diffset_container_id = h.md5(diffset.target_ref)
collapse_all = len(diffset.files) > collapse_when_files_over
active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
%>
%if use_comments:
@ -159,45 +161,45 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
</div>
% endif
## comments
<div class="pull-right">
<div class="comments-number" style="padding-left: 10px">
% if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
<i class="icon-comment" style="color: #949494">COMMENTS:</i>
% if c.comments:
<a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
% else:
${_('0 General')}
% endif
% if c.inline_cnt:
<a href="#" onclick="return Rhodecode.comments.nextComment();"
id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
</a>
% else:
${_('0 Inline')}
% endif
% endif
% if pull_request_menu:
<%
outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
%>
% if outdated_comm_count_ver:
<a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
(${_("{} Outdated").format(outdated_comm_count_ver)})
</a>
<a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
<a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
% else:
(${_("{} Outdated").format(outdated_comm_count_ver)})
% endif
% endif
</div>
</div>
## ## comments
## <div class="pull-right">
## <div class="comments-number" style="padding-left: 10px">
## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
## % if c.comments:
## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
## % else:
## ${_('0 General')}
## % endif
##
## % if c.inline_cnt:
## <a href="#" onclick="return Rhodecode.comments.nextComment();"
## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
## </a>
## % else:
## ${_('0 Inline')}
## % endif
## % endif
##
## % if pull_request_menu:
## <%
## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
## %>
##
## % if outdated_comm_count_ver:
## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
## (${_("{} Outdated").format(outdated_comm_count_ver)})
## </a>
## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
## % else:
## (${_("{} Outdated").format(outdated_comm_count_ver)})
## % endif
##
## % endif
##
## </div>
## </div>
</div>
@ -208,13 +210,6 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
<a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
</h2>
</div>
## commit range header for each individual diff
% elif commit and hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
<div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
<div class="clearinner">
<a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a>
</div>
</div>
% endif
<div id="todo-box">
@ -239,6 +234,43 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
<% over_lines_changed_limit = False %>
%for i, filediff in enumerate(diffset.files):
%if filediff.source_file_path and filediff.target_file_path:
%if filediff.source_file_path != filediff.target_file_path:
## file was renamed, or copied
%if RENAMED_FILENODE in filediff.patch['stats']['ops']:
<%
final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> <del>{}</del>'.format(filediff.target_file_path, filediff.source_file_path))
final_path = filediff.target_file_path
%>
%elif COPIED_FILENODE in filediff.patch['stats']['ops']:
<%
final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> {}'.format(filediff.target_file_path, filediff.source_file_path))
final_path = filediff.target_file_path
%>
%endif
%else:
## file was modified
<%
final_file_name = filediff.source_file_path
final_path = final_file_name
%>
%endif
%else:
%if filediff.source_file_path:
## file was deleted
<%
final_file_name = filediff.source_file_path
final_path = final_file_name
%>
%else:
## file was added
<%
final_file_name = filediff.target_file_path
final_path = final_file_name
%>
%endif
%endif
<%
lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
over_lines_changed_limit = lines_changed > lines_changed_limit
@ -258,13 +290,39 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
%>
<div class="filediff-collapse-indicator icon-"></div>
<span class="pill-group pull-right" >
<span class="pill" op="comments">
## Comments/Options PILL
<span class="pill-group pull-right">
<span class="pill" op="comments">
<i class="icon-comment"></i> ${len(total_file_comments)}
</span>
<details class="details-reset details-inline-block">
<summary class="noselect">
<i class="pill icon-options cursor-pointer" op="options"></i>
</summary>
<details-menu class="details-dropdown">
<div class="dropdown-item">
<span>${final_path}</span>
<span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item">
<% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
<a href="${permalink}">¶ permalink</a>
<span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
</div>
</details-menu>
</details>
</span>
${diff_ops(filediff)}
${diff_ops(final_file_name, filediff)}
</label>
@ -463,43 +521,15 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
</div>
</%def>
<%def name="diff_ops(filediff)">
<%
from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
%>
<%def name="diff_ops(file_name, filediff)">
<%
from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
%>
<span class="pill">
<i class="icon-file-text"></i>
%if filediff.source_file_path and filediff.target_file_path:
%if filediff.source_file_path != filediff.target_file_path:
## file was renamed, or copied
%if RENAMED_FILENODE in filediff.patch['stats']['ops']:
${filediff.target_file_path} ⬅ <del>${filediff.source_file_path}</del>
<% final_path = filediff.target_file_path %>
%elif COPIED_FILENODE in filediff.patch['stats']['ops']:
${filediff.target_file_path} ⬅ ${filediff.source_file_path}
<% final_path = filediff.target_file_path %>
%endif
%else:
## file was modified
${filediff.source_file_path}
<% final_path = filediff.source_file_path %>
%endif
%else:
%if filediff.source_file_path:
## file was deleted
${filediff.source_file_path}
<% final_path = filediff.source_file_path %>
%else:
## file was added
${filediff.target_file_path}
<% final_path = filediff.target_file_path %>
%endif
%endif
<i style="color: #aaa" class="on-hover-icon icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy file path')}" onclick="return false;"></i>
${file_name}
</span>
## anchor link
<a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">¶</a>
<span class="pill-group pull-right">
@ -934,7 +964,7 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
</span>
%endif
% if commit or pull_request_menu:
<span id="diff_nav">Loading diff...:</span>
<span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
<span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
<i class="icon-angle-up"></i>
</span>

View file

@ -21,8 +21,9 @@
## 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)
urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns, issues_container=getattr(c, 'referenced_commit_issues', None))
%>
%for commit in c.commit_ranges:
<tr id="row-${commit.raw_id}"
commit_id="${commit.raw_id}"

View file

@ -1,6 +1,10 @@
<%text>
<div style="display: none">
<script>
var CG = new ColorGenerator();
</script>
<script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
<%
@ -34,38 +38,41 @@ var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
</script>
<script>
var CG = new ColorGenerator();
</script>
<script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
<%
if (create) {
var edit_visibility = 'visible';
} else {
var edit_visibility = 'hidden';
}
<li id="reviewer_<%= member.user_id %>" class="reviewer_entry">
<%
if (create) {
var edit_visibility = 'visible';
} else {
var edit_visibility = 'hidden';
}
if (member.user_group && member.user_group.vote_rule) {
var reviewGroup = '<i class="icon-user-group"></i>';
var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
} else {
var reviewGroup = null;
var reviewGroupColor = 'transparent';
}
var rule_show = rule_show || false;
if (member.user_group && member.user_group.vote_rule) {
var groupStyle = 'border-left: 1px solid '+CG.asRGB(CG.getColor(member.user_group.vote_rule));
} else {
var groupStyle = 'border-left: 1px solid white';
}
%>
if (rule_show) {
var rule_visibility = 'table-cell';
} else {
var rule_visibility = 'none';
}
<div class="reviewers_member" style="<%= groupStyle%>" >
%>
<tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
<td style="width: 20px">
<div class="reviewer_status tooltip" title="<%= review_status_label %>">
<i class="icon-circle review-status-<%= review_status %>"></i>
</div>
<div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
<% if (mandatory) { %>
<div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
<i class="icon-lock"></i>
</div>
<% } %>
</td>
<td>
<div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
<%-
renderTemplate('gravatarWithUser', {
'size': 16,
@ -77,12 +84,44 @@ var CG = new ColorGenerator();
'gravatar_url': member.gravatar_link
})
%>
<span class="tooltip presence-state" style="display: none" title="This users is currently at this page">
<i class="icon-eye" style="color: #0ac878"></i>
</span>
</div>
</td>
<td style="width: 10px">
<% if (reviewGroup !== null) { %>
<span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
<%- reviewGroup %>
</span>
<% } %>
</td>
<% if (mandatory) { %>
<td style="text-align: right;width: 10px;">
<div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
<i class="icon-lock"></i>
</div>
</td>
<% } else { %>
<td style="text-align: right;width: 10px;">
<% if (allowed_to_update) { %>
<div class="reviewer_member_remove" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
<i class="icon-remove"></i>
</div>
<% } %>
</td>
<% } %>
</tr>
<tr>
<td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
<input type="hidden" name="__start__" value="reviewer:mapping">
<%if (member.user_group && member.user_group.vote_rule) {%>
<%if (member.user_group && member.user_group.vote_rule) { %>
<div class="reviewer_reason">
<%if (member.user_group.vote_rule == -1) {%>
@ -91,7 +130,7 @@ var CG = new ColorGenerator();
- group votes required: <%= member.user_group.vote_rule %>
<%}%>
</div>
<%}%>
<%} %>
<input type="hidden" name="__start__" value="reasons:sequence">
<% for (var i = 0; i < reasons.length; i++) { %>
@ -99,37 +138,24 @@ var CG = new ColorGenerator();
<div class="reviewer_reason">- <%= reason %></div>
<input type="hidden" name="reason" value="<%= reason %>">
<% } %>
<input type="hidden" name="__end__" value="reasons:sequence">
<input type="hidden" name="__end__" value="reasons:sequence">
<input type="hidden" name="__start__" value="rules:sequence">
<input type="hidden" name="__start__" value="rules:sequence">
<% for (var i = 0; i < member.rules.length; i++) { %>
<% var rule = member.rules[i] %>
<input type="hidden" name="rule_id" value="<%= rule %>">
<% } %>
<input type="hidden" name="__end__" value="rules:sequence">
<input type="hidden" name="__end__" value="rules:sequence">
<input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
<input type="hidden" name="mandatory" value="<%= mandatory %>"/>
<input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
<input type="hidden" name="mandatory" value="<%= mandatory %>"/>
<input type="hidden" name="__end__" value="reviewer:mapping">
<% if (mandatory) { %>
<div class="reviewer_member_mandatory_remove" style="visibility: <%= edit_visibility %>;">
<i class="icon-remove"></i>
</div>
<% } else { %>
<% if (allowed_to_update) { %>
<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
<i class="icon-remove" ></i>
</div>
<% } %>
<% } %>
</div>
</li>
</td>
</tr>
</script>
<script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
<%
@ -158,8 +184,56 @@ if (show_disabled) {
</script>
<script id="ejs_sideBarCommentHovercard" type="text/template" class="ejsTemplate">
<div>
<% if (is_todo) { %>
<% if (inline) { %>
<strong>Inline</strong> TODO on line: <%= line_no %>
<% if (version_info) { %>
<%= version_info %>
<% } %>
<br/>
File: <code><%- file_name -%></code>
<% } else { %>
<% if (review_status) { %>
<i class="icon-circle review-status-<%= review_status %>"></i>
<% } %>
<strong>General</strong> TODO
<% if (version_info) { %>
<%= version_info %>
<% } %>
<% } %>
<% } else { %>
<% if (inline) { %>
<strong>Inline</strong> comment on line: <%= line_no %>
<% if (version_info) { %>
<%= version_info %>
<% } %>
<br/>
File: <code><%- file_name -%></code>
<% } else { %>
<% if (review_status) { %>
<i class="icon-circle review-status-<%= review_status %>"></i>
<% } %>
<strong>General</strong> comment
<% if (version_info) { %>
<%= version_info %>
<% } %>
<% } %>
<% } %>
<br/>
Created:
<time class="timeago" title="<%= created_on %>" datetime="<%= datetime %>"><%= $.timeago(datetime) %></time>
</div>
</script>
##// END OF EJS Templates
</div>
<script>
// registers the templates into global cache
registerTemplates();

View file

@ -360,13 +360,13 @@ ${self.plaintext_footer()}
div.markdown-block ul.checkbox li, div.markdown-block ol.checkbox li {
list-style: none !important;
margin: 6px !important;
margin: 0px !important;
padding: 0 !important
}
div.markdown-block ul li, div.markdown-block ol li {
list-style: disc !important;
margin: 6px !important;
margin: 0px !important;
padding: 0 !important
}

View file

@ -19,21 +19,74 @@
<div class="box">
${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
<div class="box pr-summary">
<div class="box">
<div class="summary-details block-left">
<div class="pr-details-title">
${_('New pull request')}
</div>
<div class="form" style="padding-top: 10px">
<!-- fields -->
<div class="fields" >
<div class="field">
## COMMIT FLOW
<div class="field">
<div class="label label-textarea">
<label for="commit_flow">${_('Commit flow')}:</label>
</div>
<div class="content">
<div class="flex-container">
<div style="width: 45%;">
<div class="panel panel-default source-panel">
<div class="panel-heading">
<h3 class="panel-title">${_('Source repository')}</h3>
</div>
<div class="panel-body">
<div style="display:none">${c.rhodecode_db_repo.description}</div>
${h.hidden('source_repo')}
${h.hidden('source_ref')}
<div id="pr_open_message"></div>
</div>
</div>
</div>
<div style="width: 90px; text-align: center; padding-top: 30px">
<div>
<i class="icon-right" style="font-size: 2.2em"></i>
</div>
<div style="position: relative; top: 10px">
<span class="tag tag">
<span id="switch_base"></span>
</span>
</div>
</div>
<div style="width: 45%;">
<div class="panel panel-default target-panel">
<div class="panel-heading">
<h3 class="panel-title">${_('Target repository')}</h3>
</div>
<div class="panel-body">
<div style="display:none" id="target_repo_desc"></div>
${h.hidden('target_repo')}
${h.hidden('target_ref')}
<span id="target_ref_loading" style="display: none">
${_('Loading refs...')}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
## TITLE
<div class="field">
<div class="label">
<label for="pullrequest_title">${_('Title')}:</label>
</div>
@ -43,8 +96,9 @@
<p class="help-block">
Start the title with WIP: to prevent accidental merge of Work In Progress pull request before it's ready.
</p>
</div>
</div>
## DESC
<div class="field">
<div class="label label-textarea">
<label for="pullrequest_desc">${_('Description')}:</label>
@ -55,39 +109,49 @@
</div>
</div>
## REVIEWERS
<div class="field">
<div class="label label-textarea">
<label for="commit_flow">${_('Commit flow')}:</label>
</div>
## TODO: johbo: Abusing the "content" class here to get the
## desired effect. Should be replaced by a proper solution.
##ORG
<div class="content">
<strong>${_('Source repository')}:</strong>
${c.rhodecode_db_repo.description}
<label for="pullrequest_reviewers">${_('Reviewers')}:</label>
</div>
<div class="content">
${h.hidden('source_repo')}
${h.hidden('source_ref')}
</div>
## REVIEW RULES
<div id="review_rules" style="display: none" class="reviewers-title">
<div class="pr-details-title">
${_('Reviewer rules')}
</div>
<div class="pr-reviewer-rules">
## review rules will be appended here, by default reviewers logic
</div>
</div>
##OTHER, most Probably the PARENT OF THIS FORK
<div class="content">
## filled with JS
<div id="target_repo_desc"></div>
</div>
## REVIEWERS
<div class="reviewers-title">
<div class="pr-details-title">
${_('Pull request reviewers')}
<span class="calculate-reviewers"> - ${_('loading...')}</span>
</div>
</div>
<div id="reviewers" class="pr-details-content reviewers">
## members goes here, filled via JS based on initial selection !
<input type="hidden" name="__start__" value="review_members:sequence">
<table id="review_members" class="group_members">
## This content is loaded via JS and ReviewersPanel
</table>
<input type="hidden" name="__end__" value="review_members:sequence">
<div class="content">
${h.hidden('target_repo')}
${h.hidden('target_ref')}
<span id="target_ref_loading" style="display: none">
${_('Loading refs...')}
</span>
<div id="add_reviewer_input" class='ac'>
<div class="reviewer_ac">
${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
<div id="reviewers_container"></div>
</div>
</div>
</div>
</div>
</div>
## SUBMIT
<div class="field">
<div class="label label-textarea">
<label for="pullrequest_submit"></label>
@ -96,66 +160,14 @@
<div class="pr-submit-button">
<input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
</div>
<div id="pr_open_message"></div>
</div>
</div>
<div class="pr-spacing-container"></div>
</div>
</div>
</div>
<div>
## AUTHOR
<div class="reviewers-title block-right">
<div class="pr-details-title">
${_('Author of this pull request')}
</div>
</div>
<div class="block-right pr-details-content reviewers">
<ul class="group_members">
<li>
${self.gravatar_with_user(c.rhodecode_user.email, 16, tooltip=True)}
</li>
</ul>
</div>
## REVIEW RULES
<div id="review_rules" style="display: none" class="reviewers-title block-right">
<div class="pr-details-title">
${_('Reviewer rules')}
</div>
<div class="pr-reviewer-rules">
## review rules will be appended here, by default reviewers logic
</div>
</div>
## REVIEWERS
<div class="reviewers-title block-right">
<div class="pr-details-title">
${_('Pull request reviewers')}
<span class="calculate-reviewers"> - ${_('loading...')}</span>
</div>
</div>
<div id="reviewers" class="block-right pr-details-content reviewers">
## members goes here, filled via JS based on initial selection !
<input type="hidden" name="__start__" value="review_members:sequence">
<ul id="review_members" class="group_members"></ul>
<input type="hidden" name="__end__" value="review_members:sequence">
<div id="add_reviewer_input" class='ac'>
<div class="reviewer_ac">
${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
<div id="reviewers_container"></div>
</div>
</div>
</div>
</div>
</div>
<div class="box">
<div>
## overview pulled by ajax
<div id="pull_request_overview"></div>
</div>
</div>
${h.end_form()}
</div>
@ -243,8 +255,6 @@
var diffDataHandler = function(data) {
$('#pull_request_overview').html(data);
var commitElements = data['commits'];
var files = data['files'];
var added = data['stats'][0]
@ -303,27 +313,33 @@
msg += '<input type="hidden" name="__end__" value="revisions:sequence">'
msg += _ngettext(
'This pull requests will consist of <strong>{0} commit</strong>.',
'This pull requests will consist of <strong>{0} commits</strong>.',
'Compare summary: <strong>{0} commit</strong>',
'Compare summary: <strong>{0} commits</strong>',
commitElements.length).format(commitElements.length)
msg += '\n';
msg += '';
msg += _ngettext(
'<strong>{0} file</strong> changed, ',
'<strong>{0} files</strong> changed, ',
'<strong>, and {0} file</strong> changed.',
'<strong>, and {0} files</strong> changed.',
files.length).format(files.length)
msg += '<span class="op-added">{0} lines inserted</span>, <span class="op-deleted">{1} lines deleted</span>.'.format(added, deleted)
msg += '\n\n <a class="" id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
msg += '\n Diff: <span class="op-added">{0} lines inserted</span>, <span class="op-deleted">{1} lines deleted </span>.'.format(added, deleted)
msg += '\n <a class="" id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
if (commitElements.length) {
var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
}
else {
prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
_gettext('There are no commits to merge.'));
prButtonLock(true, noCommitsMsg, 'compare');
}
//make both panels equal
$('.target-panel').height($('.source-panel').height())
};
reviewersController = new ReviewersController();
@ -429,10 +445,12 @@
var targetRepoChanged = function(repoData) {
// generate new DESC of target repo displayed next to select
$('#target_repo_desc').html(repoData['description']);
var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
$('#target_repo_desc').html(
"<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Switch base, and use as source.</a>".format(repoData['description'], prLink)
);
var title = _gettext('Switch target repository with the source.')
$('#switch_base').html("<a class=\"tooltip\" title=\"{0}\" href=\"{1}\">Switch sides</a>".format(title, prLink))
// generate dynamic select2 for refs.
initTargetRefs(repoData['refs']['select2_refs'],

View file

@ -1,6 +1,8 @@
<%inherit file="/base/base.mako"/>
<%namespace name="base" file="/base/base.mako"/>
<%namespace name="dt" file="/data_table/_dt_elements.mako"/>
<%namespace name="sidebar" file="/base/sidebar.mako"/>
<%def name="title()">
${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
@ -21,12 +23,19 @@
${self.repo_menu(active='showpullrequest')}
</%def>
<%def name="main()">
## Container to gather extracted Tickets
<%
c.referenced_commit_issues = []
c.referenced_desc_issues = []
%>
<script type="text/javascript">
// TODO: marcink switch this to pyroutes
AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
</script>
<div class="box">
@ -79,7 +88,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)}
${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container=c.referenced_desc_issues)}
</div>
<div id="pr-desc-edit" class="input textarea" style="display: none;">
@ -89,29 +98,6 @@
<div id="summary" class="fields pr-details-content">
## review
<div class="field">
<div class="label-pr-detail">
<label>${_('Review status')}:</label>
</div>
<div class="input">
%if c.pull_request_review_status:
<div class="tag status-tag-${c.pull_request_review_status}">
<i class="icon-circle review-status-${c.pull_request_review_status}"></i>
<span class="changeset-status-lbl">
%if c.pull_request.is_closed():
${_('Closed')},
%endif
${h.commit_status_lbl(c.pull_request_review_status)}
</span>
</div>
- ${_ungettext('calculated based on {} reviewer vote', 'calculated based on {} reviewers votes', len(c.pull_request_reviewers)).format(len(c.pull_request_reviewers))}
%endif
</div>
</div>
## source
<div class="field">
<div class="label-pr-detail">
@ -136,7 +122,7 @@
${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
<a class="source-details-action" href="#expand-source-details" onclick="return versionController.toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
<a class="source-details-action" href="#expand-source-details" onclick="return toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
<i class="icon-angle-down">more details</i>
</a>
@ -231,7 +217,7 @@
</code>
</td>
<td>
<input ${('checked="checked"' if c.from_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
<input ${('checked="checked"' if c.from_version_index == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
<input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
</td>
<td>
@ -280,159 +266,12 @@
</div>
## REVIEW RULES
<div id="review_rules" style="display: none" class="reviewers-title block-right">
<div class="pr-details-title">
${_('Reviewer rules')}
%if c.allowed_to_update:
<span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
%endif
</div>
<div class="pr-reviewer-rules">
## review rules will be appended here, by default reviewers logic
</div>
<input id="review_data" type="hidden" name="review_data" value="">
</div>
## REVIEWERS
<div class="reviewers-title first-panel block-right">
<div class="pr-details-title">
${_('Pull request reviewers')}
%if c.allowed_to_update:
<span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
%endif
</div>
</div>
<div id="reviewers" class="block-right pr-details-content reviewers">
## members redering block
<input type="hidden" name="__start__" value="review_members:sequence">
<ul id="review_members" class="group_members">
% for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
<script>
var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
var status = "${(status[0][1].status if status else 'not_reviewed')}";
var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
var entry = renderTemplate('reviewMemberEntry', {
'member': member,
'mandatory': member.mandatory,
'reasons': member.reasons,
'allowed_to_update': allowed_to_update,
'review_status': status,
'review_status_label': status_lbl,
'user_group': member.user_group,
'create': false
});
$('#review_members').append(entry)
</script>
% endfor
</ul>
<input type="hidden" name="__end__" value="review_members:sequence">
## end members redering block
%if not c.pull_request.is_closed():
<div id="add_reviewer" class="ac" style="display: none;">
%if c.allowed_to_update:
% if not c.forbid_adding_reviewers:
<div id="add_reviewer_input" class="reviewer_ac">
${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
<div id="reviewers_container"></div>
</div>
% endif
<div class="pull-right">
<button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
</div>
%endif
</div>
%endif
</div>
## TODOs will be listed here
<div class="reviewers-title block-right">
<div class="pr-details-title">
## Only show unresolved, that is only what matters
TODO Comments - ${len(c.unresolved_comments)} / ${(len(c.unresolved_comments) + len(c.resolved_comments))}
% if not c.at_version:
% if c.resolved_comments:
<span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return versionController.toggleElement(this, '.unresolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
% else:
<span class="block-right last-item noselect">Show resolved</span>
% endif
% endif
</div>
</div>
<div class="block-right pr-details-content reviewers">
<table class="todo-table">
<%
def sorter(entry):
user_id = entry.author.user_id
resolved = '1' if entry.resolved else '0'
if user_id == c.rhodecode_user.user_id:
# own comments first
user_id = 0
return '{}_{}_{}'.format(resolved, user_id, str(entry.comment_id).zfill(100))
%>
% if c.at_version:
<tr>
<td class="unresolved-todo-text">${_('unresolved TODOs unavailable in this view')}.</td>
</tr>
% else:
% for todo_comment in sorted(c.unresolved_comments + c.resolved_comments, key=sorter):
<% resolved = todo_comment.resolved %>
% if inline:
<% outdated_at_ver = todo_comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
% else:
<% outdated_at_ver = todo_comment.older_than_version(getattr(c, 'at_version_num', None)) %>
% endif
<tr ${('class="unresolved-todo" style="display: none"' if resolved else '') |n}>
<td class="td-todo-number">
% if resolved:
<a class="permalink todo-resolved tooltip" title="${_('Resolved by comment #{}').format(todo_comment.resolved.comment_id)}" href="#comment-${todo_comment.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${todo_comment.comment_id}'), 0, ${h.json.dumps(outdated_at_ver)})">
<i class="icon-flag-filled"></i> ${todo_comment.comment_id}</a>
% else:
<a class="permalink" href="#comment-${todo_comment.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${todo_comment.comment_id}'), 0, ${h.json.dumps(outdated_at_ver)})">
<i class="icon-flag-filled"></i> ${todo_comment.comment_id}</a>
% endif
</td>
<td class="td-todo-gravatar">
${base.gravatar(todo_comment.author.email, 16, user=todo_comment.author, tooltip=True, extra_class=['no-margin'])}
</td>
<td class="todo-comment-text-wrapper">
<div class="todo-comment-text">
<code>${h.chop_at_smart(todo_comment.text, '\n', suffix_if_chopped='...')}</code>
</div>
</td>
</tr>
% endfor
% if len(c.unresolved_comments) == 0:
<tr>
<td class="unresolved-todo-text">${_('No unresolved TODOs')}.</td>
</tr>
% endif
% endif
</table>
</div>
</div>
</div>
<div class="box">
</div>
<div class="box">
% if c.state_progressing:
@ -484,9 +323,9 @@
<div class="compare_view_commits_title">
% if not c.compare_mode:
% if c.at_version_pos:
% if c.at_version_index:
<h4>
${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
</h4>
% endif
@ -539,10 +378,11 @@
</div>
% if not c.missing_commits:
## COMPARE RANGE DIFF MODE
% if c.compare_mode:
% if c.at_version:
<h4>
${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_index, ver_to=c.at_version_index if c.at_version_index else 'latest')}:
</h4>
<div class="subtitle-compare">
@ -597,7 +437,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)}</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=c.referenced_commit_issues)}</div>
</div>
</td>
</tr>
@ -608,19 +448,13 @@
% endif
## Regular DIFF
% else:
<%include file="/compare/compare_commits.mako" />
% endif
<div class="cs_files">
<%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
% if c.at_version:
<% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
<% c.comments = c.comment_versions[c.at_version_num]['display'] %>
% else:
<% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
<% c.comments = c.comment_versions[c.at_version_num]['until'] %>
% endif
<%
pr_menu_data = {
@ -667,7 +501,7 @@
## comments heading with count
<div class="comments-heading">
<i class="icon-comment"></i>
${_('Comments')} ${len(c.comments)}
${_('General Comments')} ${len(c.comments)}
</div>
## render general comments
@ -704,218 +538,456 @@
% endif
</div>
<script type="text/javascript">
versionController = new VersionController();
versionController.init();
### NAV SIDEBAR
<aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
<div class="sidenav navbar__inner" >
## TOGGLE
<div class="sidebar-toggle" onclick="toggleSidebar(); return false">
<a href="#toggleSidebar" class="grey-link-action">
reviewersController = new ReviewersController();
commitsController = new CommitsController();
</a>
</div>
updateController = new UpdatePrController();
## CONTENT
<div class="sidebar-content">
$(function () {
## RULES SUMMARY/RULES
<div class="sidebar-element clear-both">
<% vote_title = _ungettext(
'Status calculated based on votes from {} reviewer',
'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
%>
// custom code mirror
var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
<div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
<i class="icon-circle review-status-${c.pull_request_review_status}"></i>
${len(c.allowed_reviewers)}
</div>
var PRDetails = {
editButton: $('#open_edit_pullrequest'),
closeButton: $('#close_edit_pullrequest'),
deleteButton: $('#delete_pullrequest'),
viewFields: $('#pr-desc, #pr-title'),
editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
## REVIEW RULES
<div id="review_rules" style="display: none" class="">
<div class="right-sidebar-expanded-state pr-details-title">
<span class="sidebar-heading">
${_('Reviewer rules')}
</span>
init: function () {
var that = this;
this.editButton.on('click', function (e) {
that.edit();
});
this.closeButton.on('click', function (e) {
that.view();
});
},
</div>
<div class="pr-reviewer-rules">
## review rules will be appended here, by default reviewers logic
</div>
<input id="review_data" type="hidden" name="review_data" value="">
</div>
edit: function (event) {
this.viewFields.hide();
this.editButton.hide();
this.deleteButton.hide();
this.closeButton.show();
this.editFields.show();
codeMirrorInstance.refresh();
},
## REVIEWERS
<div class="right-sidebar-expanded-state pr-details-title">
<span class="tooltip sidebar-heading" title="${vote_title}">
<i class="icon-circle review-status-${c.pull_request_review_status}"></i>
${_('Reviewers')}
</span>
%if c.allowed_to_update:
<span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
<span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
%else:
<span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
<span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
%endif
</div>
view: function (event) {
this.editButton.show();
this.deleteButton.show();
this.editFields.hide();
this.closeButton.hide();
this.viewFields.show();
}
};
<div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
var ReviewersPanel = {
editButton: $('#open_edit_reviewers'),
closeButton: $('#close_edit_reviewers'),
addButton: $('#add_reviewer'),
removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
## members redering block
<input type="hidden" name="__start__" value="review_members:sequence">
init: function () {
var self = this;
this.editButton.on('click', function (e) {
self.edit();
});
this.closeButton.on('click', function (e) {
self.close();
});
},
<table id="review_members" class="group_members">
## This content is loaded via JS and ReviewersPanel
</table>
edit: function (event) {
this.editButton.hide();
this.closeButton.show();
this.addButton.show();
this.removeButtons.css('visibility', 'visible');
// review rules
reviewersController.loadReviewRules(
${c.pull_request.reviewer_data_json | n});
},
<input type="hidden" name="__end__" value="review_members:sequence">
## end members redering block
close: function (event) {
this.editButton.show();
this.closeButton.hide();
this.addButton.hide();
this.removeButtons.css('visibility', 'hidden');
// hide review rules
reviewersController.hideReviewRules()
}
};
%if not c.pull_request.is_closed():
<div id="add_reviewer" class="ac" style="display: none;">
%if c.allowed_to_update:
% if not c.forbid_adding_reviewers:
<div id="add_reviewer_input" class="reviewer_ac">
${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
<div id="reviewers_container"></div>
</div>
% endif
<div class="pull-right">
<button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
</div>
%endif
</div>
%endif
</div>
</div>
PRDetails.init();
ReviewersPanel.init();
## ## OBSERVERS
## <div class="sidebar-element clear-both">
## <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
## <i class="icon-eye"></i>
## 0
## </div>
##
## <div class="right-sidebar-expanded-state pr-details-title">
## <span class="sidebar-heading">
## <i class="icon-eye"></i>
## ${_('Observers')}
## </span>
## </div>
## <div class="right-sidebar-expanded-state pr-details-content">
## No observers
## </div>
## </div>
showOutdated = function (self) {
$('.comment-inline.comment-outdated').show();
$('.filediff-outdated').show();
$('.showOutdatedComments').hide();
$('.hideOutdatedComments').show();
};
## TODOs
<div class="sidebar-element clear-both">
<div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
<i class="icon-flag-filled"></i>
<span id="todos-count">${len(c.unresolved_comments)}</span>
</div>
hideOutdated = function (self) {
$('.comment-inline.comment-outdated').hide();
$('.filediff-outdated').hide();
$('.hideOutdatedComments').hide();
$('.showOutdatedComments').show();
};
<div class="right-sidebar-expanded-state pr-details-title">
## Only show unresolved, that is only what matters
<span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
<i class="icon-flag-filled"></i>
TODOs
</span>
refreshMergeChecks = function () {
var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
$('.pull-request-merge').css('opacity', 0.3);
$('.action-buttons-extra').css('opacity', 0.3);
% if not c.at_version:
% if c.resolved_comments:
<span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
% else:
<span class="block-right last-item noselect">Show resolved</span>
% endif
% endif
</div>
$('.pull-request-merge').load(
loadUrl, function () {
$('.pull-request-merge').css('opacity', 1);
<div class="right-sidebar-expanded-state pr-details-content">
$('.action-buttons-extra').css('opacity', 1);
}
);
};
% if c.at_version:
<table>
<tr>
<td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
</tr>
</table>
% else:
% if c.unresolved_comments + c.resolved_comments:
${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
% else:
<table>
<tr>
<td>
${_('No TODOs yet')}
</td>
</tr>
</table>
% endif
% endif
</div>
</div>
closePullRequest = function (status) {
if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
return false;
}
// inject closing flag
$('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
$(generalCommentForm.statusChange).select2("val", status).trigger('change');
$(generalCommentForm.submitForm).submit();
};
## COMMENTS
<div class="sidebar-element clear-both">
<div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
<i class="icon-comment" style="color: #949494"></i>
<span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
<span class="display-none" id="general-comments-count">${len(c.comments)}</span>
<span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
</div>
$('#show-outdated-comments').on('click', function (e) {
var button = $(this);
var outdated = $('.comment-outdated');
<div class="right-sidebar-expanded-state pr-details-title">
<span class="sidebar-heading noselect" onclick="refreshComments(); return false">
<i class="icon-comment" style="color: #949494"></i>
${_('Comments')}
if (button.html() === "(Show)") {
button.html("(Hide)");
outdated.show();
} else {
button.html("(Show)");
outdated.hide();
}
});
## % if outdated_comm_count_ver:
## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
## (${_("{} Outdated").format(outdated_comm_count_ver)})
## </a>
## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
$('.show-inline-comments').on('change', function (e) {
var show = 'none';
var target = e.currentTarget;
if (target.checked) {
show = ''
}
var boxid = $(target).attr('id_for');
var comments = $('#{0} .inline-comments'.format(boxid));
var fn_display = function (idx) {
$(this).css('display', show);
};
$(comments).each(fn_display);
var btns = $('#{0} .inline-comments-button'.format(boxid));
$(btns).each(fn_display);
});
## % else:
## (${_("{} Outdated").format(outdated_comm_count_ver)})
## % endif
$('#merge_pull_request_form').submit(function () {
if (!$('#merge_pull_request').attr('disabled')) {
$('#merge_pull_request').attr('disabled', 'disabled');
}
return true;
});
</span>
$('#edit_pull_request').on('click', function (e) {
var title = $('#pr-title-input').val();
var description = codeMirrorInstance.getValue();
var renderer = $('#pr-renderer-input').val();
editPullRequest(
"${c.repo_name}", "${c.pull_request.pull_request_id}",
title, description, renderer);
});
% if outdated_comm_count_ver:
<span class="block-right action_button last-item noselect" onclick="return toggleElement(this, '.hidden-comment');" data-toggle-on="Show outdated" data-toggle-off="Hide outdated">Show outdated</span>
% else:
<span class="block-right last-item noselect">Show hidden</span>
% endif
$('#update_pull_request').on('click', function (e) {
$(this).attr('disabled', 'disabled');
$(this).addClass('disabled');
$(this).html(_gettext('Saving...'));
reviewersController.updateReviewers(
"${c.repo_name}", "${c.pull_request.pull_request_id}");
});
</div>
<div class="right-sidebar-expanded-state pr-details-content">
% if c.inline_comments_flat + c.comments:
${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
% else:
<table>
<tr>
<td>
${_('No Comments yet')}
</td>
</tr>
</table>
% endif
</div>
// fixing issue with caches on firefox
$('#update_commits').removeAttr("disabled");
</div>
$('.show-inline-comments').on('click', function (e) {
var boxid = $(this).attr('data-comment-id');
var button = $(this);
## Referenced Tickets
<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))}
</div>
if (button.hasClass("comments-visible")) {
$('#{0} .inline-comments'.format(boxid)).each(function (index) {
$(this).hide();
});
button.removeClass("comments-visible");
} else {
$('#{0} .inline-comments'.format(boxid)).each(function (index) {
$(this).show();
});
button.addClass("comments-visible");
}
});
<div class="right-sidebar-expanded-state pr-details-title">
<span class="sidebar-heading">
<i class="icon-info-circled"></i>
${_('Referenced Tickets')}
</span>
</div>
<div class="right-sidebar-expanded-state pr-details-content">
<table>
// register submit callback on commentForm form to track TODOs
window.commentFormGlobalSubmitSuccessCallback = function () {
refreshMergeChecks();
};
<tr><td><code>${_('In pull request description')}:</code></td></tr>
% if c.referenced_desc_issues:
% for ticket_dict in c.referenced_desc_issues:
<tr>
<td>
<a href="${ticket_dict.get('url')}">
${ticket_dict.get('id')}
</a>
</td>
</tr>
% endfor
% else:
<tr>
<td>
${_('No Ticket data found.')}
</td>
</tr>
% endif
ReviewerAutoComplete('#user');
<tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
% if c.referenced_commit_issues:
% for ticket_dict in c.referenced_commit_issues:
<tr>
<td>
<a href="${ticket_dict.get('url')}">
${ticket_dict.get('id')}
</a>
</td>
</tr>
% endfor
% else:
<tr>
<td>
${_('No Ticket data found.')}
</td>
</tr>
% endif
</table>
})
</div>
</div>
</script>
</div>
</div>
</div>
</aside>
## This JS needs to be at the end
<script type="text/javascript">
versionController = new VersionController();
versionController.init();
reviewersController = new ReviewersController();
commitsController = new CommitsController();
updateController = new UpdatePrController();
window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
(function () {
"use strict";
// custom code mirror
var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
var PRDetails = {
editButton: $('#open_edit_pullrequest'),
closeButton: $('#close_edit_pullrequest'),
deleteButton: $('#delete_pullrequest'),
viewFields: $('#pr-desc, #pr-title'),
editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
init: function () {
var that = this;
this.editButton.on('click', function (e) {
that.edit();
});
this.closeButton.on('click', function (e) {
that.view();
});
},
edit: function (event) {
var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
this.viewFields.hide();
this.editButton.hide();
this.deleteButton.hide();
this.closeButton.show();
this.editFields.show();
cmInstance.refresh();
},
view: function (event) {
this.editButton.show();
this.deleteButton.show();
this.editFields.hide();
this.closeButton.hide();
this.viewFields.show();
}
};
PRDetails.init();
ReviewersPanel.init(reviewerRulesData, setReviewersData);
window.showOutdated = function (self) {
$('.comment-inline.comment-outdated').show();
$('.filediff-outdated').show();
$('.showOutdatedComments').hide();
$('.hideOutdatedComments').show();
};
window.hideOutdated = function (self) {
$('.comment-inline.comment-outdated').hide();
$('.filediff-outdated').hide();
$('.hideOutdatedComments').hide();
$('.showOutdatedComments').show();
};
window.refreshMergeChecks = function () {
var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
$('.pull-request-merge').css('opacity', 0.3);
$('.action-buttons-extra').css('opacity', 0.3);
$('.pull-request-merge').load(
loadUrl, function () {
$('.pull-request-merge').css('opacity', 1);
$('.action-buttons-extra').css('opacity', 1);
}
);
};
window.closePullRequest = function (status) {
if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
return false;
}
// inject closing flag
$('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
$(generalCommentForm.statusChange).select2("val", status).trigger('change');
$(generalCommentForm.submitForm).submit();
};
//TODO this functionality is now missing
$('#show-outdated-comments').on('click', function (e) {
var button = $(this);
var outdated = $('.comment-outdated');
if (button.html() === "(Show)") {
button.html("(Hide)");
outdated.show();
} else {
button.html("(Show)");
outdated.hide();
}
});
$('#merge_pull_request_form').submit(function () {
if (!$('#merge_pull_request').attr('disabled')) {
$('#merge_pull_request').attr('disabled', 'disabled');
}
return true;
});
$('#edit_pull_request').on('click', function (e) {
var title = $('#pr-title-input').val();
var description = codeMirrorInstance.getValue();
var renderer = $('#pr-renderer-input').val();
editPullRequest(
"${c.repo_name}", "${c.pull_request.pull_request_id}",
title, description, renderer);
});
$('#update_pull_request').on('click', function (e) {
$(this).attr('disabled', 'disabled');
$(this).addClass('disabled');
$(this).html(_gettext('Saving...'));
reviewersController.updateReviewers(
"${c.repo_name}", "${c.pull_request.pull_request_id}");
});
// fixing issue with caches on firefox
$('#update_commits').removeAttr("disabled");
$('.show-inline-comments').on('click', function (e) {
var boxid = $(this).attr('data-comment-id');
var button = $(this);
if (button.hasClass("comments-visible")) {
$('#{0} .inline-comments'.format(boxid)).each(function (index) {
$(this).hide();
});
button.removeClass("comments-visible");
} else {
$('#{0} .inline-comments'.format(boxid)).each(function (index) {
$(this).show();
});
button.addClass("comments-visible");
}
});
$('.show-inline-comments').on('change', function (e) {
var show = 'none';
var target = e.currentTarget;
if (target.checked) {
show = ''
}
var boxid = $(target).attr('id_for');
var comments = $('#{0} .inline-comments'.format(boxid));
var fn_display = function (idx) {
$(this).css('display', show);
};
$(comments).each(fn_display);
var btns = $('#{0} .inline-comments-button'.format(boxid));
$(btns).each(fn_display);
});
// register submit callback on commentForm form to track TODOs
window.commentFormGlobalSubmitSuccessCallback = function () {
refreshMergeChecks();
};
ReviewerAutoComplete('#user');
})();
$(document).ready(function () {
var channel = '${c.pr_broadcast_channel}';
new ReviewerPresenceController(channel)
})
</script>
</%def>

View file

@ -948,8 +948,8 @@ def assert_inline_comments(pull_request, visible=None, outdated=None):
if visible is not None:
inline_comments = CommentsModel().get_inline_comments(
pull_request.target_repo.repo_id, pull_request=pull_request)
inline_cnt = CommentsModel().get_inline_comments_count(
inline_comments)
inline_cnt = len(CommentsModel().get_inline_comments_as_list(
inline_comments))
assert inline_cnt == visible
if outdated is not None:
outdated_comments = CommentsModel().get_outdated_comments(