Show More
The requested changes are too big and content was truncated. Show full diff
@@ -0,0 +1,46 b'' | |||
|
1 | |RCE| 4.24.1 |RNS| | |
|
2 | ------------------ | |
|
3 | ||
|
4 | Release Date | |
|
5 | ^^^^^^^^^^^^ | |
|
6 | ||
|
7 | - 2021-02-04 | |
|
8 | ||
|
9 | ||
|
10 | New Features | |
|
11 | ^^^^^^^^^^^^ | |
|
12 | ||
|
13 | ||
|
14 | ||
|
15 | General | |
|
16 | ^^^^^^^ | |
|
17 | ||
|
18 | - Core: added statsd client for statistics usage. | |
|
19 | - Clone urls: allow custom clone by id template so users can set clone-by-id as default. | |
|
20 | - Automation: enable check for new version for EE edition as automation task that will send notifications when new RhodeCode version is available | |
|
21 | ||
|
22 | Security | |
|
23 | ^^^^^^^^ | |
|
24 | ||
|
25 | ||
|
26 | ||
|
27 | Performance | |
|
28 | ^^^^^^^^^^^ | |
|
29 | ||
|
30 | - Core: bumped git to 2.30.0 | |
|
31 | ||
|
32 | ||
|
33 | Fixes | |
|
34 | ^^^^^ | |
|
35 | ||
|
36 | - Comments: add ability to resolve todos from the side-bar. This should prevent situations | |
|
37 | when a TODO was left over in outdated/removed code pieces, and users needs to search to resolve them. | |
|
38 | - Pull requests: fixed a case when template marker was used in description field causing 500 errors on commenting. | |
|
39 | - Merges: fixed excessive data saved in merge metadata that could not fit inside the DB table. | |
|
40 | - Exceptions: fixed problem with exceptions formatting resulting in limited exception data reporting. | |
|
41 | ||
|
42 | ||
|
43 | Upgrade notes | |
|
44 | ^^^^^^^^^^^^^ | |
|
45 | ||
|
46 | - Un-scheduled release addressing problems in 4.24.X releases. |
@@ -0,0 +1,12 b'' | |||
|
1 | diff -rup pytest-4.6.5-orig/setup.py pytest-4.6.5/setup.py | |
|
2 | --- pytest-4.6.5-orig/setup.py 2018-04-10 10:23:04.000000000 +0200 | |
|
3 | +++ pytest-4.6.5/setup.py 2018-04-10 10:23:34.000000000 +0200 | |
|
4 | @@ -24,7 +24,7 @@ INSTALL_REQUIRES = [ | |
|
5 | def main(): | |
|
6 | setup( | |
|
7 | use_scm_version={"write_to": "src/_pytest/_version.py"}, | |
|
8 | - setup_requires=["setuptools-scm", "setuptools>=40.0"], | |
|
9 | + setup_requires=["setuptools-scm", "setuptools<=42.0"], | |
|
10 | package_dir={"": "src"}, | |
|
11 | # fmt: off | |
|
12 | extras_require={ No newline at end of file |
@@ -0,0 +1,46 b'' | |||
|
1 | from __future__ import absolute_import, division, unicode_literals | |
|
2 | ||
|
3 | import logging | |
|
4 | ||
|
5 | from .stream import TCPStatsClient, UnixSocketStatsClient # noqa | |
|
6 | from .udp import StatsClient # noqa | |
|
7 | ||
|
8 | HOST = 'localhost' | |
|
9 | PORT = 8125 | |
|
10 | IPV6 = False | |
|
11 | PREFIX = None | |
|
12 | MAXUDPSIZE = 512 | |
|
13 | ||
|
14 | log = logging.getLogger('rhodecode.statsd') | |
|
15 | ||
|
16 | ||
|
17 | def statsd_config(config, prefix='statsd.'): | |
|
18 | _config = {} | |
|
19 | for key in config.keys(): | |
|
20 | if key.startswith(prefix): | |
|
21 | _config[key[len(prefix):]] = config[key] | |
|
22 | return _config | |
|
23 | ||
|
24 | ||
|
25 | def client_from_config(configuration, prefix='statsd.', **kwargs): | |
|
26 | from pyramid.settings import asbool | |
|
27 | ||
|
28 | _config = statsd_config(configuration, prefix) | |
|
29 | statsd_enabled = asbool(_config.pop('enabled', False)) | |
|
30 | if not statsd_enabled: | |
|
31 | log.debug('statsd client not enabled by statsd.enabled = flag, skipping...') | |
|
32 | return | |
|
33 | ||
|
34 | host = _config.pop('statsd_host', HOST) | |
|
35 | port = _config.pop('statsd_port', PORT) | |
|
36 | prefix = _config.pop('statsd_prefix', PREFIX) | |
|
37 | maxudpsize = _config.pop('statsd_maxudpsize', MAXUDPSIZE) | |
|
38 | ipv6 = asbool(_config.pop('statsd_ipv6', IPV6)) | |
|
39 | log.debug('configured statsd client %s:%s', host, port) | |
|
40 | ||
|
41 | return StatsClient( | |
|
42 | host=host, port=port, prefix=prefix, maxudpsize=maxudpsize, ipv6=ipv6) | |
|
43 | ||
|
44 | ||
|
45 | def get_statsd_client(request): | |
|
46 | return client_from_config(request.registry.settings) |
@@ -0,0 +1,107 b'' | |||
|
1 | from __future__ import absolute_import, division, unicode_literals | |
|
2 | ||
|
3 | import random | |
|
4 | from collections import deque | |
|
5 | from datetime import timedelta | |
|
6 | ||
|
7 | from .timer import Timer | |
|
8 | ||
|
9 | ||
|
10 | class StatsClientBase(object): | |
|
11 | """A Base class for various statsd clients.""" | |
|
12 | ||
|
13 | def close(self): | |
|
14 | """Used to close and clean up any underlying resources.""" | |
|
15 | raise NotImplementedError() | |
|
16 | ||
|
17 | def _send(self): | |
|
18 | raise NotImplementedError() | |
|
19 | ||
|
20 | def pipeline(self): | |
|
21 | raise NotImplementedError() | |
|
22 | ||
|
23 | def timer(self, stat, rate=1): | |
|
24 | return Timer(self, stat, rate) | |
|
25 | ||
|
26 | def timing(self, stat, delta, rate=1): | |
|
27 | """ | |
|
28 | Send new timing information. | |
|
29 | ||
|
30 | `delta` can be either a number of milliseconds or a timedelta. | |
|
31 | """ | |
|
32 | if isinstance(delta, timedelta): | |
|
33 | # Convert timedelta to number of milliseconds. | |
|
34 | delta = delta.total_seconds() * 1000. | |
|
35 | self._send_stat(stat, '%0.6f|ms' % delta, rate) | |
|
36 | ||
|
37 | def incr(self, stat, count=1, rate=1): | |
|
38 | """Increment a stat by `count`.""" | |
|
39 | self._send_stat(stat, '%s|c' % count, rate) | |
|
40 | ||
|
41 | def decr(self, stat, count=1, rate=1): | |
|
42 | """Decrement a stat by `count`.""" | |
|
43 | self.incr(stat, -count, rate) | |
|
44 | ||
|
45 | def gauge(self, stat, value, rate=1, delta=False): | |
|
46 | """Set a gauge value.""" | |
|
47 | if value < 0 and not delta: | |
|
48 | if rate < 1: | |
|
49 | if random.random() > rate: | |
|
50 | return | |
|
51 | with self.pipeline() as pipe: | |
|
52 | pipe._send_stat(stat, '0|g', 1) | |
|
53 | pipe._send_stat(stat, '%s|g' % value, 1) | |
|
54 | else: | |
|
55 | prefix = '+' if delta and value >= 0 else '' | |
|
56 | self._send_stat(stat, '%s%s|g' % (prefix, value), rate) | |
|
57 | ||
|
58 | def set(self, stat, value, rate=1): | |
|
59 | """Set a set value.""" | |
|
60 | self._send_stat(stat, '%s|s' % value, rate) | |
|
61 | ||
|
62 | def _send_stat(self, stat, value, rate): | |
|
63 | self._after(self._prepare(stat, value, rate)) | |
|
64 | ||
|
65 | def _prepare(self, stat, value, rate): | |
|
66 | if rate < 1: | |
|
67 | if random.random() > rate: | |
|
68 | return | |
|
69 | value = '%s|@%s' % (value, rate) | |
|
70 | ||
|
71 | if self._prefix: | |
|
72 | stat = '%s.%s' % (self._prefix, stat) | |
|
73 | ||
|
74 | return '%s:%s' % (stat, value) | |
|
75 | ||
|
76 | def _after(self, data): | |
|
77 | if data: | |
|
78 | self._send(data) | |
|
79 | ||
|
80 | ||
|
81 | class PipelineBase(StatsClientBase): | |
|
82 | ||
|
83 | def __init__(self, client): | |
|
84 | self._client = client | |
|
85 | self._prefix = client._prefix | |
|
86 | self._stats = deque() | |
|
87 | ||
|
88 | def _send(self): | |
|
89 | raise NotImplementedError() | |
|
90 | ||
|
91 | def _after(self, data): | |
|
92 | if data is not None: | |
|
93 | self._stats.append(data) | |
|
94 | ||
|
95 | def __enter__(self): | |
|
96 | return self | |
|
97 | ||
|
98 | def __exit__(self, typ, value, tb): | |
|
99 | self.send() | |
|
100 | ||
|
101 | def send(self): | |
|
102 | if not self._stats: | |
|
103 | return | |
|
104 | self._send() | |
|
105 | ||
|
106 | def pipeline(self): | |
|
107 | return self.__class__(self) |
@@ -0,0 +1,75 b'' | |||
|
1 | from __future__ import absolute_import, division, unicode_literals | |
|
2 | ||
|
3 | import socket | |
|
4 | ||
|
5 | from .base import StatsClientBase, PipelineBase | |
|
6 | ||
|
7 | ||
|
8 | class StreamPipeline(PipelineBase): | |
|
9 | def _send(self): | |
|
10 | self._client._after('\n'.join(self._stats)) | |
|
11 | self._stats.clear() | |
|
12 | ||
|
13 | ||
|
14 | class StreamClientBase(StatsClientBase): | |
|
15 | def connect(self): | |
|
16 | raise NotImplementedError() | |
|
17 | ||
|
18 | def close(self): | |
|
19 | if self._sock and hasattr(self._sock, 'close'): | |
|
20 | self._sock.close() | |
|
21 | self._sock = None | |
|
22 | ||
|
23 | def reconnect(self): | |
|
24 | self.close() | |
|
25 | self.connect() | |
|
26 | ||
|
27 | def pipeline(self): | |
|
28 | return StreamPipeline(self) | |
|
29 | ||
|
30 | def _send(self, data): | |
|
31 | """Send data to statsd.""" | |
|
32 | if not self._sock: | |
|
33 | self.connect() | |
|
34 | self._do_send(data) | |
|
35 | ||
|
36 | def _do_send(self, data): | |
|
37 | self._sock.sendall(data.encode('ascii') + b'\n') | |
|
38 | ||
|
39 | ||
|
40 | class TCPStatsClient(StreamClientBase): | |
|
41 | """TCP version of StatsClient.""" | |
|
42 | ||
|
43 | def __init__(self, host='localhost', port=8125, prefix=None, | |
|
44 | timeout=None, ipv6=False): | |
|
45 | """Create a new client.""" | |
|
46 | self._host = host | |
|
47 | self._port = port | |
|
48 | self._ipv6 = ipv6 | |
|
49 | self._timeout = timeout | |
|
50 | self._prefix = prefix | |
|
51 | self._sock = None | |
|
52 | ||
|
53 | def connect(self): | |
|
54 | fam = socket.AF_INET6 if self._ipv6 else socket.AF_INET | |
|
55 | family, _, _, _, addr = socket.getaddrinfo( | |
|
56 | self._host, self._port, fam, socket.SOCK_STREAM)[0] | |
|
57 | self._sock = socket.socket(family, socket.SOCK_STREAM) | |
|
58 | self._sock.settimeout(self._timeout) | |
|
59 | self._sock.connect(addr) | |
|
60 | ||
|
61 | ||
|
62 | class UnixSocketStatsClient(StreamClientBase): | |
|
63 | """Unix domain socket version of StatsClient.""" | |
|
64 | ||
|
65 | def __init__(self, socket_path, prefix=None, timeout=None): | |
|
66 | """Create a new client.""" | |
|
67 | self._socket_path = socket_path | |
|
68 | self._timeout = timeout | |
|
69 | self._prefix = prefix | |
|
70 | self._sock = None | |
|
71 | ||
|
72 | def connect(self): | |
|
73 | self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | |
|
74 | self._sock.settimeout(self._timeout) | |
|
75 | self._sock.connect(self._socket_path) |
@@ -0,0 +1,71 b'' | |||
|
1 | from __future__ import absolute_import, division, unicode_literals | |
|
2 | ||
|
3 | import functools | |
|
4 | ||
|
5 | # Use timer that's not susceptible to time of day adjustments. | |
|
6 | try: | |
|
7 | # perf_counter is only present on Py3.3+ | |
|
8 | from time import perf_counter as time_now | |
|
9 | except ImportError: | |
|
10 | # fall back to using time | |
|
11 | from time import time as time_now | |
|
12 | ||
|
13 | ||
|
14 | def safe_wraps(wrapper, *args, **kwargs): | |
|
15 | """Safely wraps partial functions.""" | |
|
16 | while isinstance(wrapper, functools.partial): | |
|
17 | wrapper = wrapper.func | |
|
18 | return functools.wraps(wrapper, *args, **kwargs) | |
|
19 | ||
|
20 | ||
|
21 | class Timer(object): | |
|
22 | """A context manager/decorator for statsd.timing().""" | |
|
23 | ||
|
24 | def __init__(self, client, stat, rate=1): | |
|
25 | self.client = client | |
|
26 | self.stat = stat | |
|
27 | self.rate = rate | |
|
28 | self.ms = None | |
|
29 | self._sent = False | |
|
30 | self._start_time = None | |
|
31 | ||
|
32 | def __call__(self, f): | |
|
33 | """Thread-safe timing function decorator.""" | |
|
34 | @safe_wraps(f) | |
|
35 | def _wrapped(*args, **kwargs): | |
|
36 | start_time = time_now() | |
|
37 | try: | |
|
38 | return f(*args, **kwargs) | |
|
39 | finally: | |
|
40 | elapsed_time_ms = 1000.0 * (time_now() - start_time) | |
|
41 | self.client.timing(self.stat, elapsed_time_ms, self.rate) | |
|
42 | return _wrapped | |
|
43 | ||
|
44 | def __enter__(self): | |
|
45 | return self.start() | |
|
46 | ||
|
47 | def __exit__(self, typ, value, tb): | |
|
48 | self.stop() | |
|
49 | ||
|
50 | def start(self): | |
|
51 | self.ms = None | |
|
52 | self._sent = False | |
|
53 | self._start_time = time_now() | |
|
54 | return self | |
|
55 | ||
|
56 | def stop(self, send=True): | |
|
57 | if self._start_time is None: | |
|
58 | raise RuntimeError('Timer has not started.') | |
|
59 | dt = time_now() - self._start_time | |
|
60 | self.ms = 1000.0 * dt # Convert to milliseconds. | |
|
61 | if send: | |
|
62 | self.send() | |
|
63 | return self | |
|
64 | ||
|
65 | def send(self): | |
|
66 | if self.ms is None: | |
|
67 | raise RuntimeError('No data recorded.') | |
|
68 | if self._sent: | |
|
69 | raise RuntimeError('Already sent data.') | |
|
70 | self._sent = True | |
|
71 | self.client.timing(self.stat, self.ms, self.rate) |
@@ -0,0 +1,55 b'' | |||
|
1 | from __future__ import absolute_import, division, unicode_literals | |
|
2 | ||
|
3 | import socket | |
|
4 | ||
|
5 | from .base import StatsClientBase, PipelineBase | |
|
6 | ||
|
7 | ||
|
8 | class Pipeline(PipelineBase): | |
|
9 | ||
|
10 | def __init__(self, client): | |
|
11 | super(Pipeline, self).__init__(client) | |
|
12 | self._maxudpsize = client._maxudpsize | |
|
13 | ||
|
14 | def _send(self): | |
|
15 | data = self._stats.popleft() | |
|
16 | while self._stats: | |
|
17 | # Use popleft to preserve the order of the stats. | |
|
18 | stat = self._stats.popleft() | |
|
19 | if len(stat) + len(data) + 1 >= self._maxudpsize: | |
|
20 | self._client._after(data) | |
|
21 | data = stat | |
|
22 | else: | |
|
23 | data += '\n' + stat | |
|
24 | self._client._after(data) | |
|
25 | ||
|
26 | ||
|
27 | class StatsClient(StatsClientBase): | |
|
28 | """A client for statsd.""" | |
|
29 | ||
|
30 | def __init__(self, host='localhost', port=8125, prefix=None, | |
|
31 | maxudpsize=512, ipv6=False): | |
|
32 | """Create a new client.""" | |
|
33 | fam = socket.AF_INET6 if ipv6 else socket.AF_INET | |
|
34 | family, _, _, _, addr = socket.getaddrinfo( | |
|
35 | host, port, fam, socket.SOCK_DGRAM)[0] | |
|
36 | self._addr = addr | |
|
37 | self._sock = socket.socket(family, socket.SOCK_DGRAM) | |
|
38 | self._prefix = prefix | |
|
39 | self._maxudpsize = maxudpsize | |
|
40 | ||
|
41 | def _send(self, data): | |
|
42 | """Send data to statsd.""" | |
|
43 | try: | |
|
44 | self._sock.sendto(data.encode('ascii'), self._addr) | |
|
45 | except (socket.error, RuntimeError): | |
|
46 | # No time for love, Dr. Jones! | |
|
47 | pass | |
|
48 | ||
|
49 | def close(self): | |
|
50 | if self._sock and hasattr(self._sock, 'close'): | |
|
51 | self._sock.close() | |
|
52 | self._sock = None | |
|
53 | ||
|
54 | def pipeline(self): | |
|
55 | return Pipeline(self) |
@@ -0,0 +1,64 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | import logging | |
|
4 | from sqlalchemy import * | |
|
5 | ||
|
6 | from alembic.migration import MigrationContext | |
|
7 | from alembic.operations import Operations | |
|
8 | ||
|
9 | from rhodecode.lib.dbmigrate.versions import _reset_base | |
|
10 | from rhodecode.model import meta, init_model_encryption | |
|
11 | ||
|
12 | ||
|
13 | log = logging.getLogger(__name__) | |
|
14 | ||
|
15 | ||
|
16 | def upgrade(migrate_engine): | |
|
17 | """ | |
|
18 | Upgrade operations go here. | |
|
19 | Don't create your own engine; bind migrate_engine to your metadata | |
|
20 | """ | |
|
21 | _reset_base(migrate_engine) | |
|
22 | from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db | |
|
23 | ||
|
24 | init_model_encryption(db) | |
|
25 | ||
|
26 | # issue fixups | |
|
27 | fixups(db, meta.Session) | |
|
28 | ||
|
29 | ||
|
30 | def downgrade(migrate_engine): | |
|
31 | meta = MetaData() | |
|
32 | meta.bind = migrate_engine | |
|
33 | ||
|
34 | ||
|
35 | def fixups(models, _SESSION): | |
|
36 | # now create new changed value of clone_url | |
|
37 | Optional = models.Optional | |
|
38 | ||
|
39 | def get_by_name(cls, key): | |
|
40 | return cls.query().filter(cls.app_settings_name == key).scalar() | |
|
41 | ||
|
42 | def create_or_update(cls, key, val=Optional(''), type_=Optional('unicode')): | |
|
43 | res = get_by_name(cls, key) | |
|
44 | if not res: | |
|
45 | val = Optional.extract(val) | |
|
46 | type_ = Optional.extract(type_) | |
|
47 | res = cls(key, val, type_) | |
|
48 | else: | |
|
49 | res.app_settings_name = key | |
|
50 | if not isinstance(val, Optional): | |
|
51 | # update if set | |
|
52 | res.app_settings_value = val | |
|
53 | if not isinstance(type_, Optional): | |
|
54 | # update if set | |
|
55 | res.app_settings_type = type_ | |
|
56 | return res | |
|
57 | ||
|
58 | clone_uri_tmpl = models.Repository.DEFAULT_CLONE_URI_ID | |
|
59 | print('settings new clone by url template to %s' % clone_uri_tmpl) | |
|
60 | ||
|
61 | sett = create_or_update(models.RhodeCodeSetting, | |
|
62 | 'clone_uri_id_tmpl', clone_uri_tmpl, 'unicode') | |
|
63 | _SESSION().add(sett) | |
|
64 | _SESSION.commit() |
@@ -0,0 +1,32 b'' | |||
|
1 | ## -*- coding: utf-8 -*- | |
|
2 | <%inherit file="base.mako"/> | |
|
3 | ||
|
4 | <%def name="subject()" filter="n,trim,whitespace_filter"> | |
|
5 | New Version of RhodeCode is available ! | |
|
6 | </%def> | |
|
7 | ||
|
8 | ## plain text version of the email. Empty by default | |
|
9 | <%def name="body_plaintext()" filter="n,trim"> | |
|
10 | A new version of RhodeCode is available! | |
|
11 | ||
|
12 | Your version: ${current_ver} | |
|
13 | New version: ${latest_ver} | |
|
14 | ||
|
15 | Release notes: | |
|
16 | ||
|
17 | https://docs.rhodecode.com/RhodeCode-Enterprise/release-notes/release-notes-${latest_ver}.html | |
|
18 | </%def> | |
|
19 | ||
|
20 | ## BODY GOES BELOW | |
|
21 | ||
|
22 | <h3>A new version of RhodeCode is available!</h3> | |
|
23 | <br/> | |
|
24 | Your version: ${current_ver}<br/> | |
|
25 | New version: <strong>${latest_ver}</strong><br/> | |
|
26 | ||
|
27 | <h4>Release notes</h4> | |
|
28 | ||
|
29 | <a href="https://docs.rhodecode.com/RhodeCode-Enterprise/release-notes/release-notes-${latest_ver}.html"> | |
|
30 | https://docs.rhodecode.com/RhodeCode-Enterprise/release-notes/release-notes-${latest_ver}.html | |
|
31 | </a> | |
|
32 |
@@ -73,3 +73,5 b' 90734aac31ee4563bbe665a43ff73190cc762275' | |||
|
73 | 73 | a9655707f7cf4146affc51c12fe5ed8e02898a57 v4.23.0 |
|
74 | 74 | 56310d93b33b97535908ef9c7b0985b89bb7fad2 v4.23.1 |
|
75 | 75 | 7637c38528fa38c1eabc1fde6a869c20995a0da7 v4.23.2 |
|
76 | 6aeb4ac3ef7f0ac699c914740dad3688c9495e83 v4.24.0 | |
|
77 | 6eaf953da06e468a4c4e5239d3d0e700bda6b163 v4.24.1 |
@@ -1,4 +1,4 b'' | |||
|
1 |
|RCE| 4.2 |
|
|
1 | |RCE| 4.24.0 |RNS| | |
|
2 | 2 | ------------------ |
|
3 | 3 | |
|
4 | 4 | Release Date |
@@ -16,14 +16,16 b' New Features' | |||
|
16 | 16 | Can be used for backups etc. |
|
17 | 17 | - Pull requests: expose commit versions in the pull-request commit list. |
|
18 | 18 | |
|
19 | ||
|
19 | 20 | General |
|
20 | 21 | ^^^^^^^ |
|
21 | 22 | |
|
22 | 23 | - Deps: bumped redis to 3.5.3 |
|
23 |
- |
|
|
24 | - Rcextensions: improve examples for some usage. | |
|
24 | 25 | - Setup: added optional parameters to apply a default license, or skip re-creation of database at install. |
|
25 | 26 | - Docs: update headers for NGINX |
|
26 | 27 | - Beaker cache: remove no longer used beaker cache init |
|
28 | - Installation: the installer no longer requires gzip and bzip packages, and works on python 2 and 3 | |
|
27 | 29 | |
|
28 | 30 | |
|
29 | 31 | Security |
@@ -9,6 +9,7 b' Release Notes' | |||
|
9 | 9 | .. toctree:: |
|
10 | 10 | :maxdepth: 1 |
|
11 | 11 | |
|
12 | release-notes-4.24.1.rst | |
|
12 | 13 | release-notes-4.24.0.rst |
|
13 | 14 | release-notes-4.23.2.rst |
|
14 | 15 | release-notes-4.23.1.rst |
@@ -274,6 +274,12 b' self: super: {' | |||
|
274 | 274 | ]; |
|
275 | 275 | }); |
|
276 | 276 | |
|
277 | "pytest" = super."pytest".override (attrs: { | |
|
278 | patches = [ | |
|
279 | ./patches/pytest/setuptools.patch | |
|
280 | ]; | |
|
281 | }); | |
|
282 | ||
|
277 | 283 | # Avoid that base packages screw up the build process |
|
278 | 284 | inherit (basePythonPackages) |
|
279 | 285 | setuptools; |
@@ -48,7 +48,7 b' PYRAMID_SETTINGS = {}' | |||
|
48 | 48 | EXTENSIONS = {} |
|
49 | 49 | |
|
50 | 50 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) |
|
51 |
__dbversion__ = 11 |
|
|
51 | __dbversion__ = 113 # defines current db version for migrations | |
|
52 | 52 | __platform__ = platform.system() |
|
53 | 53 | __license__ = 'AGPLv3, and Commercial License' |
|
54 | 54 | __author__ = 'RhodeCode GmbH' |
@@ -384,6 +384,7 b' class AdminSettingsView(BaseAppView):' | |||
|
384 | 384 | ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'), |
|
385 | 385 | ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'), |
|
386 | 386 | ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'), |
|
387 | ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'), | |
|
387 | 388 | ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'), |
|
388 | 389 | ('support_url', 'rhodecode_support_url', 'unicode'), |
|
389 | 390 | ('show_revision_number', 'rhodecode_show_revision_number', 'bool'), |
@@ -102,6 +102,11 b' Check if we should use full-topic or min' | |||
|
102 | 102 | 'date': datetime.datetime.now(), |
|
103 | 103 | }, |
|
104 | 104 | |
|
105 | 'update_available': { | |
|
106 | 'current_ver': '4.23.0', | |
|
107 | 'latest_ver': '4.24.0', | |
|
108 | }, | |
|
109 | ||
|
105 | 110 | 'exception': { |
|
106 | 111 | 'email_prefix': '[RHODECODE ERROR]', |
|
107 | 112 | 'exc_id': exc_traceback['exc_id'], |
@@ -420,6 +420,27 b' class TestPullrequestsView(object):' | |||
|
420 | 420 | assert pull_request.title == 'New title' |
|
421 | 421 | assert pull_request.description == 'New description' |
|
422 | 422 | |
|
423 | def test_edit_title_description(self, pr_util, csrf_token): | |
|
424 | pull_request = pr_util.create_pull_request() | |
|
425 | pull_request_id = pull_request.pull_request_id | |
|
426 | ||
|
427 | response = self.app.post( | |
|
428 | route_path('pullrequest_update', | |
|
429 | repo_name=pull_request.target_repo.repo_name, | |
|
430 | pull_request_id=pull_request_id), | |
|
431 | params={ | |
|
432 | 'edit_pull_request': 'true', | |
|
433 | 'title': 'New title {} {2} {foo}', | |
|
434 | 'description': 'New description', | |
|
435 | 'csrf_token': csrf_token}) | |
|
436 | ||
|
437 | assert_session_flash( | |
|
438 | response, u'Pull request title & description updated.', | |
|
439 | category='success') | |
|
440 | ||
|
441 | pull_request = PullRequest.get(pull_request_id) | |
|
442 | assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}' | |
|
443 | ||
|
423 | 444 | def test_edit_title_description_closed(self, pr_util, csrf_token): |
|
424 | 445 | pull_request = pr_util.create_pull_request() |
|
425 | 446 | pull_request_id = pull_request.pull_request_id |
@@ -83,14 +83,10 b' class RepoSummaryView(RepoAppView):' | |||
|
83 | 83 | if self._rhodecode_user.username != User.DEFAULT_USER: |
|
84 | 84 | username = safe_str(self._rhodecode_user.username) |
|
85 | 85 | |
|
86 |
_def_clone_uri |
|
|
86 | _def_clone_uri = c.clone_uri_tmpl | |
|
87 | _def_clone_uri_id = c.clone_uri_id_tmpl | |
|
87 | 88 | _def_clone_uri_ssh = c.clone_uri_ssh_tmpl |
|
88 | 89 | |
|
89 | if '{repo}' in _def_clone_uri: | |
|
90 | _def_clone_uri_id = _def_clone_uri.replace('{repo}', '_{repoid}') | |
|
91 | elif '{repoid}' in _def_clone_uri: | |
|
92 | _def_clone_uri_id = _def_clone_uri.replace('_{repoid}', '{repo}') | |
|
93 | ||
|
94 | 90 | c.clone_repo_url = self.db_repo.clone_url( |
|
95 | 91 | user=username, uri_tmpl=_def_clone_uri) |
|
96 | 92 | c.clone_repo_url_id = self.db_repo.clone_url( |
@@ -340,6 +340,10 b' def includeme(config, auth_resources=Non' | |||
|
340 | 340 | 'rhodecode.lib.request_counter.get_request_counter', |
|
341 | 341 | 'request_count') |
|
342 | 342 | |
|
343 | config.add_request_method( | |
|
344 | 'rhodecode.lib._vendor.statsd.get_statsd_client', | |
|
345 | 'statsd', reify=True) | |
|
346 | ||
|
343 | 347 | # Set the authorization policy. |
|
344 | 348 | authz_policy = ACLAuthorizationPolicy() |
|
345 | 349 | config.set_authorization_policy(authz_policy) |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
@@ -342,6 +342,7 b' def attach_context_attributes(context, r' | |||
|
342 | 342 | if request.GET.get('default_encoding'): |
|
343 | 343 | context.default_encodings.insert(0, request.GET.get('default_encoding')) |
|
344 | 344 | context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl') |
|
345 | context.clone_uri_id_tmpl = rc_config.get('rhodecode_clone_uri_id_tmpl') | |
|
345 | 346 | context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl') |
|
346 | 347 | |
|
347 | 348 | # INI stored |
@@ -33,9 +33,9 b' from email.utils import formatdate' | |||
|
33 | 33 | |
|
34 | 34 | import rhodecode |
|
35 | 35 | from rhodecode.lib import audit_logger |
|
36 | from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask | |
|
36 | from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask, run_task | |
|
37 | 37 | from rhodecode.lib import hooks_base |
|
38 | from rhodecode.lib.utils2 import safe_int, str2bool | |
|
38 | from rhodecode.lib.utils2 import safe_int, str2bool, aslist | |
|
39 | 39 | from rhodecode.model.db import ( |
|
40 | 40 | Session, IntegrityError, true, Repository, RepoGroup, User) |
|
41 | 41 | |
@@ -338,15 +338,39 b' def repo_maintenance(repoid):' | |||
|
338 | 338 | |
|
339 | 339 | |
|
340 | 340 | @async_task(ignore_result=True) |
|
341 | def check_for_update(): | |
|
341 | def check_for_update(send_email_notification=True, email_recipients=None): | |
|
342 | 342 | from rhodecode.model.update import UpdateModel |
|
343 | from rhodecode.model.notification import EmailNotificationModel | |
|
344 | ||
|
345 | log = get_logger(check_for_update) | |
|
343 | 346 | update_url = UpdateModel().get_update_url() |
|
344 | 347 | cur_ver = rhodecode.__version__ |
|
345 | 348 | |
|
346 | 349 | try: |
|
347 | 350 | data = UpdateModel().get_update_data(update_url) |
|
348 | latest = data['versions'][0] | |
|
349 |
UpdateModel().store_version( |
|
|
351 | ||
|
352 | current_ver = UpdateModel().get_stored_version(fallback=cur_ver) | |
|
353 | latest_ver = data['versions'][0]['version'] | |
|
354 | UpdateModel().store_version(latest_ver) | |
|
355 | ||
|
356 | if send_email_notification: | |
|
357 | log.debug('Send email notification is enabled. ' | |
|
358 | 'Current RhodeCode version: %s, latest known: %s', current_ver, latest_ver) | |
|
359 | if UpdateModel().is_outdated(current_ver, latest_ver): | |
|
360 | ||
|
361 | email_kwargs = { | |
|
362 | 'current_ver': current_ver, | |
|
363 | 'latest_ver': latest_ver, | |
|
364 | } | |
|
365 | ||
|
366 | (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email( | |
|
367 | EmailNotificationModel.TYPE_UPDATE_AVAILABLE, **email_kwargs) | |
|
368 | ||
|
369 | email_recipients = aslist(email_recipients, sep=',') or \ | |
|
370 | [user.email for user in User.get_all_super_admins()] | |
|
371 | run_task(send_email, email_recipients, subject, | |
|
372 | email_body_plaintext, email_body) | |
|
373 | ||
|
350 | 374 | except Exception: |
|
351 | 375 | pass |
|
352 | 376 |
@@ -595,12 +595,13 b' class DbManage(object):' | |||
|
595 | 595 | # Visual |
|
596 | 596 | ('show_public_icon', True, 'bool'), |
|
597 | 597 | ('show_private_icon', True, 'bool'), |
|
598 |
('stylify_metatags', |
|
|
598 | ('stylify_metatags', True, 'bool'), | |
|
599 | 599 | ('dashboard_items', 100, 'int'), |
|
600 | 600 | ('admin_grid_items', 25, 'int'), |
|
601 | 601 | |
|
602 | 602 | ('markup_renderer', 'markdown', 'unicode'), |
|
603 | 603 | |
|
604 | ('repository_fields', True, 'bool'), | |
|
604 | 605 | ('show_version', True, 'bool'), |
|
605 | 606 | ('show_revision_number', True, 'bool'), |
|
606 | 607 | ('show_sha_length', 12, 'int'), |
@@ -609,6 +610,7 b' class DbManage(object):' | |||
|
609 | 610 | ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'), |
|
610 | 611 | |
|
611 | 612 | ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'), |
|
613 | ('clone_uri_id_tmpl', Repository.DEFAULT_CLONE_URI_ID, 'unicode'), | |
|
612 | 614 | ('clone_uri_ssh_tmpl', Repository.DEFAULT_CLONE_URI_SSH, 'unicode'), |
|
613 | 615 | ('support_url', '', 'unicode'), |
|
614 | 616 | ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'), |
@@ -37,21 +37,29 b' class RequestWrapperTween(object):' | |||
|
37 | 37 | |
|
38 | 38 | # one-time configuration code goes here |
|
39 | 39 | |
|
40 | def _get_user_info(self, request): | |
|
41 | user = get_current_rhodecode_user(request) | |
|
42 | if not user: | |
|
43 | user = AuthUser.repr_user(ip=get_ip_addr(request.environ)) | |
|
44 | return user | |
|
45 | ||
|
40 | 46 | def __call__(self, request): |
|
41 | 47 | start = time.time() |
|
42 | 48 | log.debug('Starting request time measurement') |
|
43 | 49 | try: |
|
44 | 50 | response = self.handler(request) |
|
45 | 51 | finally: |
|
46 | end = time.time() | |
|
47 | total = end - start | |
|
48 | 52 | count = request.request_count() |
|
49 | 53 | _ver_ = rhodecode.__version__ |
|
50 | default_user_info = AuthUser.repr_user(ip=get_ip_addr(request.environ)) | |
|
51 | user_info = get_current_rhodecode_user(request) or default_user_info | |
|
54 | statsd = request.statsd | |
|
55 | total = time.time() - start | |
|
56 | if statsd: | |
|
57 | statsd.timing('rhodecode.req.timing', total) | |
|
58 | statsd.incr('rhodecode.req.count') | |
|
59 | ||
|
52 | 60 | log.info( |
|
53 | 61 | 'Req[%4s] %s %s Request to %s time: %.4fs [%s], RhodeCode %s', |
|
54 | count, user_info, request.environ.get('REQUEST_METHOD'), | |
|
62 | count, self._get_user_info(request), request.environ.get('REQUEST_METHOD'), | |
|
55 | 63 | safe_str(get_access_path(request.environ)), total, |
|
56 | 64 | get_user_agent(request. environ), _ver_ |
|
57 | 65 | ) |
@@ -765,7 +765,14 b' class MercurialRepository(BaseRepository' | |||
|
765 | 765 | |
|
766 | 766 | try: |
|
767 | 767 | if target_ref.type == 'branch' and len(self._heads(target_ref.name)) != 1: |
|
768 |
heads = |
|
|
768 | heads_all = self._heads(target_ref.name) | |
|
769 | max_heads = 10 | |
|
770 | if len(heads_all) > max_heads: | |
|
771 | heads = '\n,'.join( | |
|
772 | heads_all[:max_heads] + | |
|
773 | ['and {} more.'.format(len(heads_all)-max_heads)]) | |
|
774 | else: | |
|
775 | heads = '\n,'.join(heads_all) | |
|
769 | 776 | metadata = { |
|
770 | 777 | 'target_ref': target_ref, |
|
771 | 778 | 'source_ref': source_ref, |
@@ -854,7 +861,16 b' class MercurialRepository(BaseRepository' | |||
|
854 | 861 | except RepositoryError as e: |
|
855 | 862 | log.exception('Failure when doing local merge on hg shadow repo') |
|
856 | 863 | if isinstance(e, UnresolvedFilesInRepo): |
|
857 |
|
|
|
864 | all_conflicts = list(e.args[0]) | |
|
865 | max_conflicts = 20 | |
|
866 | if len(all_conflicts) > max_conflicts: | |
|
867 | conflicts = all_conflicts[:max_conflicts] \ | |
|
868 | + ['and {} more.'.format(len(all_conflicts)-max_conflicts)] | |
|
869 | else: | |
|
870 | conflicts = all_conflicts | |
|
871 | metadata['unresolved_files'] = \ | |
|
872 | '\n* conflict: ' + \ | |
|
873 | ('\n * conflict: '.join(conflicts)) | |
|
858 | 874 | |
|
859 | 875 | merge_possible = False |
|
860 | 876 | merge_failure_reason = MergeFailureReason.MERGE_FAILED |
@@ -220,7 +220,7 b' def map_vcs_exceptions(func):' | |||
|
220 | 220 | if any(e.args): |
|
221 | 221 | _args = [a for a in e.args] |
|
222 | 222 | # replace the first argument with a prefix exc name |
|
223 | args = ['{}:'.format(exc_name, _args[0] if _args else '?')] + _args[1:] | |
|
223 | args = ['{}:{}'.format(exc_name, _args[0] if _args else '?')] + _args[1:] | |
|
224 | 224 | else: |
|
225 | 225 | args = [__traceback_info__ or '{}: UnhandledException'.format(exc_name)] |
|
226 | 226 | if debug or __traceback_info__ and kind not in ['unhandled', 'lookup']: |
@@ -336,6 +336,7 b' class CommentsModel(BaseModel):' | |||
|
336 | 336 | comment.author = user |
|
337 | 337 | resolved_comment = self.__get_commit_comment( |
|
338 | 338 | validated_kwargs['resolves_comment_id']) |
|
339 | ||
|
339 | 340 | # check if the comment actually belongs to this PR |
|
340 | 341 | if resolved_comment and resolved_comment.pull_request and \ |
|
341 | 342 | resolved_comment.pull_request != pull_request: |
@@ -351,6 +352,10 b' class CommentsModel(BaseModel):' | |||
|
351 | 352 | # comment not bound to this repo, forbid |
|
352 | 353 | resolved_comment = None |
|
353 | 354 | |
|
355 | if resolved_comment and resolved_comment.resolved_by: | |
|
356 | # if this comment is already resolved, don't mark it again! | |
|
357 | resolved_comment = None | |
|
358 | ||
|
354 | 359 | comment.resolved_comment = resolved_comment |
|
355 | 360 | |
|
356 | 361 | pull_request_id = pull_request |
@@ -4220,6 +4220,12 b' class _PullRequestBase(BaseModel):' | |||
|
4220 | 4220 | return True |
|
4221 | 4221 | return False |
|
4222 | 4222 | |
|
4223 | @property | |
|
4224 | def title_safe(self): | |
|
4225 | return self.title\ | |
|
4226 | .replace('{', '{{')\ | |
|
4227 | .replace('}', '}}') | |
|
4228 | ||
|
4223 | 4229 | @hybrid_property |
|
4224 | 4230 | def description_safe(self): |
|
4225 | 4231 | from rhodecode.lib import helpers as h |
@@ -390,6 +390,7 b' def ApplicationVisualisationForm(localiz' | |||
|
390 | 390 | rhodecode_markup_renderer = v.OneOf(['markdown', 'rst']) |
|
391 | 391 | rhodecode_gravatar_url = v.UnicodeString(min=3) |
|
392 | 392 | rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI) |
|
393 | rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID) | |
|
393 | 394 | rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH) |
|
394 | 395 | rhodecode_support_url = v.UnicodeString() |
|
395 | 396 | rhodecode_show_revision_number = v.StringBoolean(if_missing=False) |
@@ -343,6 +343,7 b' class EmailNotificationModel(BaseModel):' | |||
|
343 | 343 | TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation' |
|
344 | 344 | TYPE_EMAIL_TEST = 'email_test' |
|
345 | 345 | TYPE_EMAIL_EXCEPTION = 'exception' |
|
346 | TYPE_UPDATE_AVAILABLE = 'update_available' | |
|
346 | 347 | TYPE_TEST = 'test' |
|
347 | 348 | |
|
348 | 349 | email_types = { |
@@ -352,6 +353,8 b' class EmailNotificationModel(BaseModel):' | |||
|
352 | 353 | 'rhodecode:templates/email_templates/test.mako', |
|
353 | 354 | TYPE_EMAIL_EXCEPTION: |
|
354 | 355 | 'rhodecode:templates/email_templates/exception_tracker.mako', |
|
356 | TYPE_UPDATE_AVAILABLE: | |
|
357 | 'rhodecode:templates/email_templates/update_available.mako', | |
|
355 | 358 | TYPE_EMAIL_TEST: |
|
356 | 359 | 'rhodecode:templates/email_templates/email_test.mako', |
|
357 | 360 | TYPE_REGISTRATION: |
@@ -60,11 +60,11 b' class UpdateModel(BaseModel):' | |||
|
60 | 60 | Session().add(setting) |
|
61 | 61 | Session().commit() |
|
62 | 62 | |
|
63 | def get_stored_version(self): | |
|
63 | def get_stored_version(self, fallback=None): | |
|
64 | 64 | obj = SettingsModel().get_setting_by_name(self.UPDATE_SETTINGS_KEY) |
|
65 | 65 | if obj: |
|
66 | 66 | return obj.app_settings_value |
|
67 | return '0.0.0' | |
|
67 | return fallback or '0.0.0' | |
|
68 | 68 | |
|
69 | 69 | def _sanitize_version(self, version): |
|
70 | 70 | """ |
@@ -25,6 +25,7 b' var firefoxAnchorFix = function() {' | |||
|
25 | 25 | } |
|
26 | 26 | }; |
|
27 | 27 | |
|
28 | ||
|
28 | 29 | var linkifyComments = function(comments) { |
|
29 | 30 | var firstCommentId = null; |
|
30 | 31 | if (comments) { |
@@ -36,6 +37,7 b' var linkifyComments = function(comments)' | |||
|
36 | 37 | } |
|
37 | 38 | }; |
|
38 | 39 | |
|
40 | ||
|
39 | 41 | var bindToggleButtons = function() { |
|
40 | 42 | $('.comment-toggle').on('click', function() { |
|
41 | 43 | $(this).parent().nextUntil('tr.line').toggle('inline-comments'); |
@@ -43,7 +45,6 b' var bindToggleButtons = function() {' | |||
|
43 | 45 | }; |
|
44 | 46 | |
|
45 | 47 | |
|
46 | ||
|
47 | 48 | var _submitAjaxPOST = function(url, postData, successHandler, failHandler) { |
|
48 | 49 | failHandler = failHandler || function() {}; |
|
49 | 50 | postData = toQueryString(postData); |
@@ -63,8 +64,6 b' var _submitAjaxPOST = function(url, post' | |||
|
63 | 64 | }; |
|
64 | 65 | |
|
65 | 66 | |
|
66 | ||
|
67 | ||
|
68 | 67 | /* Comment form for main and inline comments */ |
|
69 | 68 | (function(mod) { |
|
70 | 69 | |
@@ -239,8 +238,7 b' var _submitAjaxPOST = function(url, post' | |||
|
239 | 238 | }; |
|
240 | 239 | |
|
241 | 240 | this.markCommentResolved = function(resolvedCommentId){ |
|
242 | $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show(); | |
|
243 | $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide(); | |
|
241 | Rhodecode.comments.markCommentResolved(resolvedCommentId) | |
|
244 | 242 | }; |
|
245 | 243 | |
|
246 | 244 | this.isAllowedToSubmit = function() { |
@@ -1308,6 +1306,11 b' var CommentsController = function() {' | |||
|
1308 | 1306 | return $(tmpl); |
|
1309 | 1307 | } |
|
1310 | 1308 | |
|
1309 | this.markCommentResolved = function(commentId) { | |
|
1310 | $('#comment-label-{0}'.format(commentId)).find('.resolved').show(); | |
|
1311 | $('#comment-label-{0}'.format(commentId)).find('.resolve').hide(); | |
|
1312 | }; | |
|
1313 | ||
|
1311 | 1314 | this.createComment = function(node, f_path, line_no, resolutionComment) { |
|
1312 | 1315 | self.edit = false; |
|
1313 | 1316 | var $node = $(node); |
@@ -1403,7 +1406,7 b' var CommentsController = function() {' | |||
|
1403 | 1406 | |
|
1404 | 1407 | //mark visually which comment was resolved |
|
1405 | 1408 | if (resolvesCommentId) { |
|
1406 |
|
|
|
1409 | self.markCommentResolved(resolvesCommentId); | |
|
1407 | 1410 | } |
|
1408 | 1411 | |
|
1409 | 1412 | // run global callback on submit |
@@ -1462,7 +1465,6 b' var CommentsController = function() {' | |||
|
1462 | 1465 | |
|
1463 | 1466 | var comment = $('#comment-'+commentId); |
|
1464 | 1467 | var commentData = comment.data(); |
|
1465 | console.log(commentData); | |
|
1466 | 1468 | |
|
1467 | 1469 | if (commentData.commentInline) { |
|
1468 | 1470 | var f_path = commentData.commentFPath; |
@@ -1494,9 +1496,144 b' var CommentsController = function() {' | |||
|
1494 | 1496 | return false; |
|
1495 | 1497 | }; |
|
1496 | 1498 | |
|
1499 | this.resolveTodo = function (elem, todoId) { | |
|
1500 | var commentId = todoId; | |
|
1501 | ||
|
1502 | SwalNoAnimation.fire({ | |
|
1503 | title: 'Resolve TODO {0}'.format(todoId), | |
|
1504 | showCancelButton: true, | |
|
1505 | confirmButtonText: _gettext('Yes'), | |
|
1506 | showLoaderOnConfirm: true, | |
|
1507 | ||
|
1508 | allowOutsideClick: function () { | |
|
1509 | !Swal.isLoading() | |
|
1510 | }, | |
|
1511 | preConfirm: function () { | |
|
1512 | var comment = $('#comment-' + commentId); | |
|
1513 | var commentData = comment.data(); | |
|
1514 | ||
|
1515 | var f_path = null | |
|
1516 | var line_no = null | |
|
1517 | if (commentData.commentInline) { | |
|
1518 | f_path = commentData.commentFPath; | |
|
1519 | line_no = commentData.commentLineNo; | |
|
1520 | } | |
|
1521 | ||
|
1522 | var renderer = templateContext.visual.default_renderer; | |
|
1523 | var commentBoxUrl = '{1}#comment-{0}'.format(commentId); | |
|
1524 | ||
|
1525 | // Pull request case | |
|
1526 | if (templateContext.pull_request_data.pull_request_id !== null) { | |
|
1527 | var commentUrl = pyroutes.url('pullrequest_comment_create', | |
|
1528 | { | |
|
1529 | 'repo_name': templateContext.repo_name, | |
|
1530 | 'pull_request_id': templateContext.pull_request_data.pull_request_id, | |
|
1531 | 'comment_id': commentId | |
|
1532 | }); | |
|
1533 | } else { | |
|
1534 | var commentUrl = pyroutes.url('repo_commit_comment_create', | |
|
1535 | { | |
|
1536 | 'repo_name': templateContext.repo_name, | |
|
1537 | 'commit_id': templateContext.commit_data.commit_id, | |
|
1538 | 'comment_id': commentId | |
|
1539 | }); | |
|
1540 | } | |
|
1541 | ||
|
1542 | if (renderer === 'rst') { | |
|
1543 | commentBoxUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentUrl); | |
|
1544 | } else if (renderer === 'markdown') { | |
|
1545 | commentBoxUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentUrl); | |
|
1546 | } | |
|
1547 | var resolveText = _gettext('TODO from comment {0} was fixed.').format(commentBoxUrl); | |
|
1548 | ||
|
1549 | var postData = { | |
|
1550 | text: resolveText, | |
|
1551 | comment_type: 'note', | |
|
1552 | draft: false, | |
|
1553 | csrf_token: CSRF_TOKEN, | |
|
1554 | resolves_comment_id: commentId | |
|
1555 | } | |
|
1556 | if (commentData.commentInline) { | |
|
1557 | postData['f_path'] = f_path; | |
|
1558 | postData['line'] = line_no; | |
|
1559 | } | |
|
1560 | ||
|
1561 | return new Promise(function (resolve, reject) { | |
|
1562 | $.ajax({ | |
|
1563 | type: 'POST', | |
|
1564 | data: postData, | |
|
1565 | url: commentUrl, | |
|
1566 | headers: {'X-PARTIAL-XHR': true} | |
|
1567 | }) | |
|
1568 | .done(function (data) { | |
|
1569 | resolve(data); | |
|
1570 | }) | |
|
1571 | .fail(function (jqXHR, textStatus, errorThrown) { | |
|
1572 | var prefix = "Error while resolving TODO.\n" | |
|
1573 | var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); | |
|
1574 | ajaxErrorSwal(message); | |
|
1575 | }); | |
|
1576 | }) | |
|
1577 | } | |
|
1578 | ||
|
1579 | }) | |
|
1580 | .then(function (result) { | |
|
1581 | var success = function (json_data) { | |
|
1582 | resolvesCommentId = commentId; | |
|
1583 | var commentResolved = json_data[Object.keys(json_data)[0]] | |
|
1584 | ||
|
1585 | try { | |
|
1586 | ||
|
1587 | if (commentResolved.f_path) { | |
|
1588 | // inject newly created comments, json_data is {<comment_id>: {}} | |
|
1589 | self.attachInlineComment(json_data) | |
|
1590 | } else { | |
|
1591 | self.attachGeneralComment(json_data) | |
|
1592 | } | |
|
1593 | ||
|
1594 | //mark visually which comment was resolved | |
|
1595 | if (resolvesCommentId) { | |
|
1596 | self.markCommentResolved(resolvesCommentId); | |
|
1597 | } | |
|
1598 | ||
|
1599 | // run global callback on submit | |
|
1600 | if (window.commentFormGlobalSubmitSuccessCallback !== undefined) { | |
|
1601 | commentFormGlobalSubmitSuccessCallback({ | |
|
1602 | draft: false, | |
|
1603 | comment_id: commentId | |
|
1604 | }); | |
|
1605 | } | |
|
1606 | ||
|
1607 | } catch (e) { | |
|
1608 | console.error(e); | |
|
1609 | } | |
|
1610 | ||
|
1611 | if (window.updateSticky !== undefined) { | |
|
1612 | // potentially our comments change the active window size, so we | |
|
1613 | // notify sticky elements | |
|
1614 | updateSticky() | |
|
1615 | } | |
|
1616 | ||
|
1617 | if (window.refreshAllComments !== undefined) { | |
|
1618 | // if we have this handler, run it, and refresh all comments boxes | |
|
1619 | refreshAllComments() | |
|
1620 | } | |
|
1621 | // re trigger the linkification of next/prev navigation | |
|
1622 | linkifyComments($('.inline-comment-injected')); | |
|
1623 | timeagoActivate(); | |
|
1624 | tooltipActivate(); | |
|
1625 | }; | |
|
1626 | ||
|
1627 | if (result.value) { | |
|
1628 | $(elem).remove(); | |
|
1629 | success(result.value) | |
|
1630 | } | |
|
1631 | }) | |
|
1632 | }; | |
|
1633 | ||
|
1497 | 1634 | }; |
|
1498 | 1635 | |
|
1499 | 1636 | window.commentHelp = function(renderer) { |
|
1500 | 1637 | var funcData = {'renderer': renderer} |
|
1501 | 1638 | return renderTemplate('commentHelpHovercard', funcData) |
|
1502 | } No newline at end of file | |
|
1639 | } |
@@ -174,6 +174,9 b'' | |||
|
174 | 174 | ${h.text('rhodecode_clone_uri_tmpl', size=60)} HTTP[S] |
|
175 | 175 | </div> |
|
176 | 176 | <div class="field"> |
|
177 | ${h.text('rhodecode_clone_uri_id_tmpl', size=60)} HTTP UID | |
|
178 | </div> | |
|
179 | <div class="field"> | |
|
177 | 180 | ${h.text('rhodecode_clone_uri_ssh_tmpl', size=60)} SSH |
|
178 | 181 | </div> |
|
179 | 182 | <div class="field"> |
@@ -236,6 +236,14 b' if (show_disabled) {' | |||
|
236 | 236 | Created: |
|
237 | 237 | <time class="timeago" title="<%= created_on %>" datetime="<%= datetime %>"><%= $.timeago(datetime) %></time> |
|
238 | 238 | |
|
239 | <% if (is_todo) { %> | |
|
240 | <div style="text-align: center; padding-top: 5px"> | |
|
241 | <a class="btn btn-sm" href="#resolveTodo<%- comment_id -%>" onclick="Rhodecode.comments.resolveTodo(this, '<%- comment_id -%>'); return false"> | |
|
242 | <strong>Resolve TODO</strong> | |
|
243 | </a> | |
|
244 | </div> | |
|
245 | <% } %> | |
|
246 | ||
|
239 | 247 | </div> |
|
240 | 248 | |
|
241 | 249 | </script> |
@@ -14,7 +14,7 b' data = {' | |||
|
14 | 14 | 'comment_type': comment_type, |
|
15 | 15 | 'comment_id': comment_id, |
|
16 | 16 | |
|
17 | 'pr_title': pull_request.title, | |
|
17 | 'pr_title': pull_request.title_safe, | |
|
18 | 18 | 'pr_id': pull_request.pull_request_id, |
|
19 | 19 | 'mention_prefix': '[mention] ' if mention else '', |
|
20 | 20 | } |
@@ -31,7 +31,6 b' else:' | |||
|
31 | 31 | _('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |
|
32 | 32 | %> |
|
33 | 33 | |
|
34 | ||
|
35 | 34 | ${subject_template.format(**data) |n} |
|
36 | 35 | </%def> |
|
37 | 36 | |
@@ -47,7 +46,7 b' data = {' | |||
|
47 | 46 | 'comment_type': comment_type, |
|
48 | 47 | 'comment_id': comment_id, |
|
49 | 48 | |
|
50 | 'pr_title': pull_request.title, | |
|
49 | 'pr_title': pull_request.title_safe, | |
|
51 | 50 | 'pr_id': pull_request.pull_request_id, |
|
52 | 51 | 'source_ref_type': pull_request.source_ref_parts.type, |
|
53 | 52 | 'source_ref_name': pull_request.source_ref_parts.name, |
@@ -99,7 +98,7 b' data = {' | |||
|
99 | 98 | 'comment_id': comment_id, |
|
100 | 99 | 'renderer_type': renderer_type or 'plain', |
|
101 | 100 | |
|
102 | 'pr_title': pull_request.title, | |
|
101 | 'pr_title': pull_request.title_safe, | |
|
103 | 102 | 'pr_id': pull_request.pull_request_id, |
|
104 | 103 | 'status': status_change, |
|
105 | 104 | 'source_ref_type': pull_request.source_ref_parts.type, |
@@ -8,7 +8,7 b'' | |||
|
8 | 8 | data = { |
|
9 | 9 | 'user': '@'+h.person(user), |
|
10 | 10 | 'pr_id': pull_request.pull_request_id, |
|
11 | 'pr_title': pull_request.title, | |
|
11 | 'pr_title': pull_request.title_safe, | |
|
12 | 12 | } |
|
13 | 13 | |
|
14 | 14 | if user_role == 'observer': |
@@ -26,7 +26,7 b' else:' | |||
|
26 | 26 | data = { |
|
27 | 27 | 'user': h.person(user), |
|
28 | 28 | 'pr_id': pull_request.pull_request_id, |
|
29 | 'pr_title': pull_request.title, | |
|
29 | 'pr_title': pull_request.title_safe, | |
|
30 | 30 | 'source_ref_type': pull_request.source_ref_parts.type, |
|
31 | 31 | 'source_ref_name': pull_request.source_ref_parts.name, |
|
32 | 32 | 'target_ref_type': pull_request.target_ref_parts.type, |
@@ -66,7 +66,7 b' data = {' | |||
|
66 | 66 | data = { |
|
67 | 67 | 'user': h.person(user), |
|
68 | 68 | 'pr_id': pull_request.pull_request_id, |
|
69 | 'pr_title': pull_request.title, | |
|
69 | 'pr_title': pull_request.title_safe, | |
|
70 | 70 | 'source_ref_type': pull_request.source_ref_parts.type, |
|
71 | 71 | 'source_ref_name': pull_request.source_ref_parts.name, |
|
72 | 72 | 'target_ref_type': pull_request.target_ref_parts.type, |
@@ -8,7 +8,7 b'' | |||
|
8 | 8 | data = { |
|
9 | 9 | 'updating_user': '@'+h.person(updating_user), |
|
10 | 10 | 'pr_id': pull_request.pull_request_id, |
|
11 | 'pr_title': pull_request.title, | |
|
11 | 'pr_title': pull_request.title_safe, | |
|
12 | 12 | } |
|
13 | 13 | |
|
14 | 14 | subject_template = email_pr_update_subject_template or _('{updating_user} updated pull request. !{pr_id}: "{pr_title}"') |
@@ -23,7 +23,7 b' subject_template = email_pr_update_subje' | |||
|
23 | 23 | data = { |
|
24 | 24 | 'updating_user': h.person(updating_user), |
|
25 | 25 | 'pr_id': pull_request.pull_request_id, |
|
26 | 'pr_title': pull_request.title, | |
|
26 | 'pr_title': pull_request.title_safe, | |
|
27 | 27 | 'source_ref_type': pull_request.source_ref_parts.type, |
|
28 | 28 | 'source_ref_name': pull_request.source_ref_parts.name, |
|
29 | 29 | 'target_ref_type': pull_request.target_ref_parts.type, |
@@ -74,7 +74,7 b' data = {' | |||
|
74 | 74 | data = { |
|
75 | 75 | 'updating_user': h.person(updating_user), |
|
76 | 76 | 'pr_id': pull_request.pull_request_id, |
|
77 | 'pr_title': pull_request.title, | |
|
77 | 'pr_title': pull_request.title_safe, | |
|
78 | 78 | 'source_ref_type': pull_request.source_ref_parts.type, |
|
79 | 79 | 'source_ref_name': pull_request.source_ref_parts.name, |
|
80 | 80 | 'target_ref_type': pull_request.target_ref_parts.type, |
@@ -27,6 +27,16 b' from rhodecode.model.db import User, Pul' | |||
|
27 | 27 | from rhodecode.model.notification import EmailNotificationModel |
|
28 | 28 | |
|
29 | 29 | |
|
30 | @pytest.fixture() | |
|
31 | def pr(): | |
|
32 | def factory(ref): | |
|
33 | return collections.namedtuple( | |
|
34 | 'PullRequest', | |
|
35 | 'pull_request_id, title, title_safe, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')\ | |
|
36 | (200, 'Example Pull Request', 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch') | |
|
37 | return factory | |
|
38 | ||
|
39 | ||
|
30 | 40 | def test_get_template_obj(app, request_stub): |
|
31 | 41 | template = EmailNotificationModel().get_renderer( |
|
32 | 42 | EmailNotificationModel.TYPE_TEST, request_stub) |
@@ -53,14 +63,10 b' def test_render_email(app, http_host_onl' | |||
|
53 | 63 | |
|
54 | 64 | |
|
55 | 65 | @pytest.mark.parametrize('role', PullRequestReviewers.ROLES) |
|
56 | def test_render_pr_email(app, user_admin, role): | |
|
66 | def test_render_pr_email(app, user_admin, role, pr): | |
|
57 | 67 | ref = collections.namedtuple( |
|
58 | 68 | 'Ref', 'name, type')('fxies123', 'book') |
|
59 | ||
|
60 | pr = collections.namedtuple('PullRequest', | |
|
61 | 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')( | |
|
62 | 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch') | |
|
63 | ||
|
69 | pr = pr(ref) | |
|
64 | 70 | source_repo = target_repo = collections.namedtuple( |
|
65 | 71 | 'Repo', 'type, repo_name')('hg', 'pull_request_1') |
|
66 | 72 | |
@@ -89,13 +95,11 b' def test_render_pr_email(app, user_admin' | |||
|
89 | 95 | assert subject == '@test_admin (RhodeCode Admin) added you as observer to pull request. !200: "Example Pull Request"' |
|
90 | 96 | |
|
91 | 97 | |
|
92 | def test_render_pr_update_email(app, user_admin): | |
|
98 | def test_render_pr_update_email(app, user_admin, pr): | |
|
93 | 99 | ref = collections.namedtuple( |
|
94 | 100 | 'Ref', 'name, type')('fxies123', 'book') |
|
95 | 101 | |
|
96 | pr = collections.namedtuple('PullRequest', | |
|
97 | 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')( | |
|
98 | 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch') | |
|
102 | pr = pr(ref) | |
|
99 | 103 | |
|
100 | 104 | source_repo = target_repo = collections.namedtuple( |
|
101 | 105 | 'Repo', 'type, repo_name')('hg', 'pull_request_1') |
@@ -150,13 +154,11 b' def test_render_pr_update_email(app, use' | |||
|
150 | 154 | EmailNotificationModel.TYPE_COMMIT_COMMENT, |
|
151 | 155 | EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT |
|
152 | 156 | ]) |
|
153 | def test_render_comment_subject_no_newlines(app, mention, email_type): | |
|
157 | def test_render_comment_subject_no_newlines(app, mention, email_type, pr): | |
|
154 | 158 | ref = collections.namedtuple( |
|
155 | 159 | 'Ref', 'name, type')('fxies123', 'book') |
|
156 | 160 | |
|
157 | pr = collections.namedtuple('PullRequest', | |
|
158 | 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')( | |
|
159 | 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch') | |
|
161 | pr = pr(ref) | |
|
160 | 162 | |
|
161 | 163 | source_repo = target_repo = collections.namedtuple( |
|
162 | 164 | 'Repo', 'type, repo_name')('hg', 'pull_request_1') |
General Comments 0
You need to be logged in to leave comments.
Login now