##// END OF EJS Templates
release: Merge default into stable for release preparation
marcink -
r4456:279b3293 merge stable
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,74 b''
1 |RCE| 4.20.0 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-07-20
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - Comments: users can now edit comments body.
14 Editing is versioned and all older versions are kept for auditing.
15 - Pull requests: changed the order of close-branch after merge,
16 so branch heads are no longer left open after the merge.
17 - Diffs: added diff navigation to improve UX when browsing the full context diffs.
18 - Emails: set the `References` header for threading in emails with different subjects.
19 Only some Email clients supports this.
20 - Emails: added logic to allow overwriting the default email titles via rcextensions.
21 - Markdown: support summary/details tags to allow setting a link with expansion menu.
22 - Integrations: added `store_file` integration. This allows storing
23 selected files from repository on disk on push.
24
25
26 General
27 ^^^^^^^
28
29 - License: individual users can hide license flash messages warning about upcoming
30 license expiration.
31 - Downloads: the default download commit is now the landing revision set in repo settings.
32 - Auth-tokens: expose all roles with explanation to help users understand it better.
33 - Pull requests: make auto generated title for pull requests show also source Ref type
34 eg. branch feature1, instead of just name of the branch.
35 - UI: added secondary action instead of two buttons on files page, and download page.
36 - Emails: reduce excessive warning logs on pre-mailer.
37
38
39 Security
40 ^^^^^^^^
41
42 - Branch permissions: protect from XSS on branch rules forbidden flash message.
43
44
45 Performance
46 ^^^^^^^^^^^
47
48
49
50 Fixes
51 ^^^^^
52
53 - Pull requests: detect missing commits on diffs from new PR ancestor logic. This fixes
54 problem with older PRs opened before 4.19.X that had special ancestor set, which could
55 lead in some cases to crash when viewing older pull requests.
56 - Permissions: fixed a case when a duplicate permission made repository settings active on archived repository.
57 - Permissions: fixed missing user info on global and repository permissions pages.
58 - Permissions: allow users to update settings for repository groups they still own,
59 or have admin perms, when they don't change their name.
60 - Permissions: flush all when running remap and rescan.
61 - Repositories: fixed a bug for repo groups that didn't pre-fill the repo group from GET param.
62 - Repositories: allow updating repository settings for users without
63 store-in-root permissions in case repository name didn't change.
64 - Comments: fixed line display icons.
65 - Summary: fixed summary page total commits count.
66
67
68 Upgrade notes
69 ^^^^^^^^^^^^^
70
71 - Schedule feature update.
72 - On Mercurial repositories we changed the order of commits when the close branch on merge features is used.
73 Before the commits was made after a merge leaving an open head.
74 This backward incompatible change now reverses that order, which is the correct way of doing it.
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -0,0 +1,35 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 from sqlalchemy import BigInteger
9
10 from rhodecode.lib.dbmigrate.versions import _reset_base
11 from rhodecode.model import init_model_encryption
12
13
14 log = logging.getLogger(__name__)
15
16
17 def upgrade(migrate_engine):
18 """
19 Upgrade operations go here.
20 Don't create your own engine; bind migrate_engine to your metadata
21 """
22 _reset_base(migrate_engine)
23 from rhodecode.lib.dbmigrate.schema import db_4_19_0_2 as db
24
25 init_model_encryption(db)
26 db.ChangesetCommentHistory().__table__.create()
27
28
29 def downgrade(migrate_engine):
30 meta = MetaData()
31 meta.bind = migrate_engine
32
33
34 def fixups(models, _SESSION):
35 pass
@@ -0,0 +1,25 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2020-2020 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 ## base64 filter e.g ${ example | base64,n }
22 def base64(text):
23 import base64
24 from rhodecode.lib.helpers import safe_str
25 return base64.encodestring(safe_str(text))
@@ -0,0 +1,91 b''
1 // jQuery Scrollstop Plugin v1.2.0
2 // https://github.com/ssorallen/jquery-scrollstop
3
4 (function (factory) {
5 // UMD[2] wrapper for jQuery plugins to work in AMD or in CommonJS.
6 //
7 // [2] https://github.com/umdjs/umd
8
9 if (typeof define === 'function' && define.amd) {
10 // AMD. Register as an anonymous module.
11 define(['jquery'], factory);
12 } else if (typeof exports === 'object') {
13 // Node/CommonJS
14 module.exports = factory(require('jquery'));
15 } else {
16 // Browser globals
17 factory(jQuery);
18 }
19 }(function ($) {
20 // $.event.dispatch was undocumented and was deprecated in jQuery 1.7[1]. It
21 // was replaced by $.event.handle in jQuery 1.9.
22 //
23 // Use the first of the available functions to support jQuery <1.8.
24 //
25 // [1] https://github.com/jquery/jquery-migrate/blob/master/src/event.js#L25
26 var dispatch = $.event.dispatch || $.event.handle;
27
28 var special = $.event.special,
29 uid1 = 'D' + (+new Date()),
30 uid2 = 'D' + (+new Date() + 1);
31
32 special.scrollstart = {
33 setup: function(data) {
34 var _data = $.extend({
35 latency: special.scrollstop.latency
36 }, data);
37
38 var timer,
39 handler = function(evt) {
40 var _self = this,
41 _args = arguments;
42
43 if (timer) {
44 clearTimeout(timer);
45 } else {
46 evt.type = 'scrollstart';
47 dispatch.apply(_self, _args);
48 }
49
50 timer = setTimeout(function() {
51 timer = null;
52 }, _data.latency);
53 };
54
55 $(this).bind('scroll', handler).data(uid1, handler);
56 },
57 teardown: function() {
58 $(this).unbind('scroll', $(this).data(uid1));
59 }
60 };
61
62 special.scrollstop = {
63 latency: 250,
64 setup: function(data) {
65 var _data = $.extend({
66 latency: special.scrollstop.latency
67 }, data);
68
69 var timer,
70 handler = function(evt) {
71 var _self = this,
72 _args = arguments;
73
74 if (timer) {
75 clearTimeout(timer);
76 }
77
78 timer = setTimeout(function() {
79 timer = null;
80 evt.type = 'scrollstop';
81 dispatch.apply(_self, _args);
82 }, _data.latency);
83 };
84
85 $(this).bind('scroll', handler).data(uid2, handler);
86 },
87 teardown: function() {
88 $(this).unbind('scroll', $(this).data(uid2));
89 }
90 };
91 }));
@@ -0,0 +1,171 b''
1 /**
2 * Within Viewport jQuery Plugin
3 *
4 * @description Companion plugin for withinviewport.js - determines whether an element is completely within the browser viewport
5 * @author Craig Patik, http://patik.com/
6 * @version 2.1.2
7 * @date 2019-08-16
8 */
9 (function ($) {
10 /**
11 * $.withinviewport()
12 * @description jQuery method
13 * @param {Object} [settings] optional settings
14 * @return {Collection} Contains all elements that were within the viewport
15 */
16 $.fn.withinviewport = function (settings) {
17 var opts;
18 var elems;
19
20 if (typeof settings === 'string') {
21 settings = {
22 sides: settings
23 };
24 }
25
26 opts = $.extend({}, settings, {
27 sides: 'all'
28 });
29 elems = [];
30
31 this.each(function () {
32 if (withinviewport(this, opts)) {
33 elems.push(this);
34 }
35 });
36
37 return $(elems);
38 };
39
40 // Main custom selector
41 $.extend($.expr[':'], {
42 'within-viewport': function (element) {
43 return withinviewport(element, 'all');
44 }
45 });
46
47 /**
48 * Optional enhancements and shortcuts
49 *
50 * @description Uncomment or comment these pieces as they apply to your project and coding preferences
51 */
52
53 // Shorthand jQuery methods
54
55 $.fn.withinviewporttop = function (settings) {
56 var opts;
57 var elems;
58
59 if (typeof settings === 'string') {
60 settings = {
61 sides: settings
62 };
63 }
64
65 opts = $.extend({}, settings, {
66 sides: 'top'
67 });
68 elems = [];
69
70 this.each(function () {
71 if (withinviewport(this, opts)) {
72 elems.push(this);
73 }
74 });
75
76 return $(elems);
77 };
78
79 $.fn.withinviewportright = function (settings) {
80 var opts;
81 var elems;
82
83 if (typeof settings === 'string') {
84 settings = {
85 sides: settings
86 };
87 }
88
89 opts = $.extend({}, settings, {
90 sides: 'right'
91 });
92 elems = [];
93
94 this.each(function () {
95 if (withinviewport(this, opts)) {
96 elems.push(this);
97 }
98 });
99
100 return $(elems);
101 };
102
103 $.fn.withinviewportbottom = function (settings) {
104 var opts;
105 var elems;
106
107 if (typeof settings === 'string') {
108 settings = {
109 sides: settings
110 };
111 }
112
113 opts = $.extend({}, settings, {
114 sides: 'bottom'
115 });
116 elems = [];
117
118 this.each(function () {
119 if (withinviewport(this, opts)) {
120 elems.push(this);
121 }
122 });
123
124 return $(elems);
125 };
126
127 $.fn.withinviewportleft = function (settings) {
128 var opts;
129 var elems;
130
131 if (typeof settings === 'string') {
132 settings = {
133 sides: settings
134 };
135 }
136
137 opts = $.extend({}, settings, {
138 sides: 'left'
139 });
140 elems = [];
141
142 this.each(function () {
143 if (withinviewport(this, opts)) {
144 elems.push(this);
145 }
146 });
147
148 return $(elems);
149 };
150
151 // Custom jQuery selectors
152 $.extend($.expr[':'], {
153 'within-viewport-top': function (element) {
154 return withinviewport(element, 'top');
155 },
156 'within-viewport-right': function (element) {
157 return withinviewport(element, 'right');
158 },
159 'within-viewport-bottom': function (element) {
160 return withinviewport(element, 'bottom');
161 },
162 'within-viewport-left': function (element) {
163 return withinviewport(element, 'left');
164 }
165 // Example custom selector:
166 //,
167 // 'within-viewport-top-left-45': function (element) {
168 // return withinviewport(element, {sides:'top left', top: 45, left: 45});
169 // }
170 });
171 }(jQuery)); No newline at end of file
@@ -0,0 +1,235 b''
1 /**
2 * Within Viewport
3 *
4 * @description Determines whether an element is completely within the browser viewport
5 * @author Craig Patik, http://patik.com/
6 * @version 2.1.2
7 * @date 2019-08-16
8 */
9 (function (root, name, factory) {
10 // AMD
11 if (typeof define === 'function' && define.amd) {
12 define([], factory);
13 }
14 // Node and CommonJS-like environments
15 else if (typeof module !== 'undefined' && typeof exports === 'object') {
16 module.exports = factory();
17 }
18 // Browser global
19 else {
20 root[name] = factory();
21 }
22 }(this, 'withinviewport', function () {
23 var canUseWindowDimensions = typeof window !== 'undefined' && window.innerHeight !== undefined; // IE 8 and lower fail this
24
25 /**
26 * Determines whether an element is within the viewport
27 * @param {Object} elem DOM Element (required)
28 * @param {Object} options Optional settings
29 * @return {Boolean} Whether the element was completely within the viewport
30 */
31 var withinviewport = function withinviewport(elem, options) {
32 var result = false;
33 var metadata = {};
34 var config = {};
35 var settings;
36 var isWithin;
37 var isContainerTheWindow;
38 var elemBoundingRect;
39 var containerBoundingRect;
40 var containerScrollTop;
41 var containerScrollLeft;
42 var scrollBarWidths = [0, 0];
43 var sideNamesPattern;
44 var sides;
45 var side;
46 var i;
47
48 // If invoked by the jQuery plugin, get the actual DOM element
49 if (typeof jQuery !== 'undefined' && elem instanceof jQuery) {
50 elem = elem.get(0);
51 }
52
53 if (typeof elem !== 'object' || elem.nodeType !== 1) {
54 throw new Error('First argument must be an element');
55 }
56
57 // Look for inline settings on the element
58 if (elem.getAttribute('data-withinviewport-settings') && window.JSON) {
59 metadata = JSON.parse(elem.getAttribute('data-withinviewport-settings'));
60 }
61
62 // Settings argument may be a simple string (`top`, `right`, etc)
63 if (typeof options === 'string') {
64 settings = {
65 sides: options
66 };
67 } else {
68 settings = options || {};
69 }
70
71 // Build configuration from defaults and user-provided settings and metadata
72 config.container = settings.container || metadata.container || withinviewport.defaults.container || window;
73 config.sides = settings.sides || metadata.sides || withinviewport.defaults.sides || 'all';
74 config.top = settings.top || metadata.top || withinviewport.defaults.top || 0;
75 config.right = settings.right || metadata.right || withinviewport.defaults.right || 0;
76 config.bottom = settings.bottom || metadata.bottom || withinviewport.defaults.bottom || 0;
77 config.left = settings.left || metadata.left || withinviewport.defaults.left || 0;
78
79 // Extract the DOM node from a jQuery collection
80 if (typeof jQuery !== 'undefined' && config.container instanceof jQuery) {
81 config.container = config.container.get(0);
82 }
83
84 // Use the window as the container if the user specified the body or a non-element
85 if (config.container === document.body || config.container.nodeType !== 1) {
86 config.container = window;
87 }
88
89 isContainerTheWindow = (config.container === window);
90
91 // Element testing methods
92 isWithin = {
93 // Element is below the top edge of the viewport
94 top: function _isWithin_top() {
95 if (isContainerTheWindow) {
96 return (elemBoundingRect.top >= config.top);
97 } else {
98 return (elemBoundingRect.top >= containerScrollTop - (containerScrollTop - containerBoundingRect.top) + config.top);
99 }
100 },
101
102 // Element is to the left of the right edge of the viewport
103 right: function _isWithin_right() {
104 // Note that `elemBoundingRect.right` is the distance from the *left* of the viewport to the element's far right edge
105
106 if (isContainerTheWindow) {
107 return (elemBoundingRect.right <= (containerBoundingRect.right + containerScrollLeft) - config.right);
108 } else {
109 return (elemBoundingRect.right <= containerBoundingRect.right - scrollBarWidths[0] - config.right);
110 }
111 },
112
113 // Element is above the bottom edge of the viewport
114 bottom: function _isWithin_bottom() {
115 var containerHeight = 0;
116
117 if (isContainerTheWindow) {
118 if (canUseWindowDimensions) {
119 containerHeight = config.container.innerHeight;
120 } else if (document && document.documentElement) {
121 containerHeight = document.documentElement.clientHeight;
122 }
123 } else {
124 containerHeight = containerBoundingRect.bottom;
125 }
126
127 // Note that `elemBoundingRect.bottom` is the distance from the *top* of the viewport to the element's bottom edge
128 return (elemBoundingRect.bottom <= containerHeight - scrollBarWidths[1] - config.bottom);
129 },
130
131 // Element is to the right of the left edge of the viewport
132 left: function _isWithin_left() {
133 if (isContainerTheWindow) {
134 return (elemBoundingRect.left >= config.left);
135 } else {
136 return (elemBoundingRect.left >= containerScrollLeft - (containerScrollLeft - containerBoundingRect.left) + config.left);
137 }
138 },
139
140 // Element is within all four boundaries
141 all: function _isWithin_all() {
142 // Test each boundary in order of efficiency and likeliness to be false. This way we can avoid running all four functions on most elements.
143 // 1. Top: Quickest to calculate + most likely to be false
144 // 2. Bottom: Note quite as quick to calculate, but also very likely to be false
145 // 3-4. Left and right are both equally unlikely to be false since most sites only scroll vertically, but left is faster to calculate
146 return (isWithin.top() && isWithin.bottom() && isWithin.left() && isWithin.right());
147 }
148 };
149
150 // Get the element's bounding rectangle with respect to the viewport
151 elemBoundingRect = elem.getBoundingClientRect();
152
153 // Get viewport dimensions and offsets
154 if (isContainerTheWindow) {
155 containerBoundingRect = document.documentElement.getBoundingClientRect();
156 containerScrollTop = document.body.scrollTop;
157 containerScrollLeft = window.scrollX || document.body.scrollLeft;
158 } else {
159 containerBoundingRect = config.container.getBoundingClientRect();
160 containerScrollTop = config.container.scrollTop;
161 containerScrollLeft = config.container.scrollLeft;
162 }
163
164 // Don't count the space consumed by scrollbars
165 if (containerScrollLeft) {
166 scrollBarWidths[0] = 18;
167 }
168
169 if (containerScrollTop) {
170 scrollBarWidths[1] = 16;
171 }
172
173 // Test the element against each side of the viewport that was requested
174 sideNamesPattern = /^top$|^right$|^bottom$|^left$|^all$/;
175
176 // Loop through all of the sides
177 sides = config.sides.split(' ');
178 i = sides.length;
179
180 while (i--) {
181 side = sides[i].toLowerCase();
182
183 if (sideNamesPattern.test(side)) {
184 if (isWithin[side]()) {
185 result = true;
186 } else {
187 result = false;
188
189 // Quit as soon as the first failure is found
190 break;
191 }
192 }
193 }
194
195 return result;
196 };
197
198 // Default settings
199 withinviewport.prototype.defaults = {
200 container: typeof document !== 'undefined' ? document.body : {},
201 sides: 'all',
202 top: 0,
203 right: 0,
204 bottom: 0,
205 left: 0
206 };
207
208 withinviewport.defaults = withinviewport.prototype.defaults;
209
210 /**
211 * Optional enhancements and shortcuts
212 *
213 * @description Uncomment or comment these pieces as they apply to your project and coding preferences
214 */
215
216 // Shortcut methods for each side of the viewport
217 // Example: `withinviewport.top(elem)` is the same as `withinviewport(elem, 'top')`
218 withinviewport.prototype.top = function _withinviewport_top(element) {
219 return withinviewport(element, 'top');
220 };
221
222 withinviewport.prototype.right = function _withinviewport_right(element) {
223 return withinviewport(element, 'right');
224 };
225
226 withinviewport.prototype.bottom = function _withinviewport_bottom(element) {
227 return withinviewport(element, 'bottom');
228 };
229
230 withinviewport.prototype.left = function _withinviewport_left(element) {
231 return withinviewport(element, 'left');
232 };
233
234 return withinviewport;
235 })); No newline at end of file
@@ -0,0 +1,31 b''
1 <%namespace name="base" file="/base/base.mako"/>
2
3 <%
4 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
5 %>
6
7 ## NOTE, inline styles are here to override the default rendering of
8 ## the swal JS dialog which this template is displayed
9
10 <div style="text-align: left;">
11
12 <div style="border-bottom: 1px solid #dbd9da; padding-bottom: 5px; height: 20px">
13
14 <div class="pull-left">
15 ${base.gravatar_with_user(c.comment_history.author.email, 16, tooltip=True)}
16 </div>
17
18 <div class="pull-right">
19 <code>edited: ${h.age_component(c.comment_history.created_on)}</code>
20 </div>
21
22 </div>
23
24 <div style="margin: 5px 0px">
25 <code>comment body at v${c.comment_history.version}:</code>
26 </div>
27 <div class="text" style="padding-top: 20px; border: 1px solid #dbd9da">
28 ${h.render(c.comment_history.text, renderer=c.comment_history.comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
29 </div>
30
31 </div> No newline at end of file
@@ -1,6 +1,5 b''
1 1 [bumpversion]
2 current_version = 4.19.3
2 current_version = 4.20.0
3 3 message = release: Bump version {current_version} to {new_version}
4 4
5 5 [bumpversion:file:rhodecode/VERSION]
6
@@ -5,25 +5,20 b' done = false'
5 5 done = true
6 6
7 7 [task:rc_tools_pinned]
8 done = true
9 8
10 9 [task:fixes_on_stable]
11 done = true
12 10
13 11 [task:pip2nix_generated]
14 done = true
15 12
16 13 [task:changelog_updated]
17 done = true
18 14
19 15 [task:generate_api_docs]
20 done = true
16
17 [task:updated_translation]
21 18
22 19 [release]
23 state = prepared
24 version = 4.19.3
25
26 [task:updated_translation]
20 state = in_progress
21 version = 4.20.0
27 22
28 23 [task:generate_js_routes]
29 24
@@ -238,7 +238,7 b' following URL: ``{instance-URL}/_admin/p'
238 238 pong[rce-7880] => 203.0.113.23
239 239
240 240 .. _Markdown: http://daringfireball.net/projects/markdown/
241 .. _reStructured Text: http://docutils.sourceforge.net/docs/index.html
241 .. _reStructured Text: http://docutils.sourceforge.io/docs/index.html
242 242
243 243
244 244 Unarchiving a repository
@@ -75,7 +75,7 b' Below config if for an Apache Reverse Pr'
75 75 # Url to running RhodeCode instance. This is shown as `- URL:` when
76 76 # running rccontrol status.
77 77
78 ProxyPass / http://127.0.0.1:10002/ timeout=7200 Keepalive=On
78 ProxyPass / http://127.0.0.1:10002/ connectiontimeout=7200 timeout=7200 Keepalive=On
79 79 ProxyPassReverse / http://127.0.0.1:10002/
80 80
81 81 # strict http prevents from https -> http downgrade
@@ -252,6 +252,7 b' get_pull_request_comments'
252 252 },
253 253 "comment_text": "Example text",
254 254 "comment_type": null,
255 "comment_last_version: 0,
255 256 "pull_request_version": null,
256 257 "comment_commit_id": None,
257 258 "comment_pull_request_id": <pull_request_id>
@@ -173,6 +173,37 b' delete_repo'
173 173 error: null
174 174
175 175
176 edit_comment
177 ------------
178
179 .. py:function:: edit_comment(apiuser, message, comment_id, version, userid=<Optional:<OptionalAttr:apiuser>>)
180
181 Edit comment on the pull request or commit,
182 specified by the `comment_id` and version. Initially version should be 0
183
184 :param apiuser: This is filled automatically from the |authtoken|.
185 :type apiuser: AuthUser
186 :param comment_id: Specify the comment_id for editing
187 :type comment_id: int
188 :param version: version of the comment that will be created, starts from 0
189 :type version: int
190 :param message: The text content of the comment.
191 :type message: str
192 :param userid: Comment on the pull request as this user
193 :type userid: Optional(str or int)
194
195 Example output:
196
197 .. code-block:: bash
198
199 id : <id_given_in_input>
200 result : {
201 "comment": "<comment data>",
202 "version": "<Integer>",
203 },
204 error : null
205
206
176 207 fork_repo
177 208 ---------
178 209
@@ -236,6 +267,40 b' fork_repo'
236 267 error: null
237 268
238 269
270 get_comment
271 -----------
272
273 .. py:function:: get_comment(apiuser, comment_id)
274
275 Get single comment from repository or pull_request
276
277 :param apiuser: This is filled automatically from the |authtoken|.
278 :type apiuser: AuthUser
279 :param comment_id: comment id found in the URL of comment
280 :type comment_id: str or int
281
282 Example error output:
283
284 .. code-block:: bash
285
286 {
287 "id" : <id_given_in_input>,
288 "result" : {
289 "comment_author": <USER_DETAILS>,
290 "comment_created_on": "2017-02-01T14:38:16.309",
291 "comment_f_path": "file.txt",
292 "comment_id": 282,
293 "comment_lineno": "n1",
294 "comment_resolved_by": null,
295 "comment_status": [],
296 "comment_text": "This file needs a header",
297 "comment_type": "todo",
298 "comment_last_version: 0
299 },
300 "error" : null
301 }
302
303
239 304 get_repo
240 305 --------
241 306
@@ -436,7 +501,8 b' get_repo_comments'
436 501 "comment_resolved_by": null,
437 502 "comment_status": [],
438 503 "comment_text": "This file needs a header",
439 "comment_type": "todo"
504 "comment_type": "todo",
505 "comment_last_version: 0
440 506 }
441 507 ],
442 508 "error" : null
@@ -11,16 +11,22 b' use the below example to insert it.'
11 11 Once configured you can check the settings for your |RCE| instance on the
12 12 :menuselection:`Admin --> Settings --> Email` page.
13 13
14 Please be aware that both section should be changed the `[DEFAULT]` for main applications
15 email config, and `[server:main]` for exception tracking email
16
14 17 .. code-block:: ini
15 18
16 ################################################################################
17 ## Uncomment and replace with the email address which should receive ##
18 ## any error reports after an application crash ##
19 ## Additionally these settings will be used by the RhodeCode mailing system ##
20 ################################################################################
21 #email_to = admin@localhost
19 [DEFAULT]
20 ; ########################################################################
21 ; EMAIL CONFIGURATION
22 ; These settings will be used by the RhodeCode mailing system
23 ; ########################################################################
24
25 ; prefix all emails subjects with given prefix, helps filtering out emails
26 #email_prefix = [RhodeCode]
27
28 ; email FROM address all mails will be sent
22 29 #app_email_from = rhodecode-noreply@localhost
23 #email_prefix = [RhodeCode]
24 30
25 31 #smtp_server = mail.server.com
26 32 #smtp_username =
@@ -28,3 +34,12 b' Once configured you can check the settin'
28 34 #smtp_port =
29 35 #smtp_use_tls = false
30 36 #smtp_use_ssl = true
37
38 [server:main]
39 ; Send email with exception details when it happens
40 #exception_tracker.send_email = true
41
42 ; Comma separated list of recipients for exception emails,
43 ; e.g admin@rhodecode.com,devops@rhodecode.com
44 ; Can be left empty, then emails will be sent to ALL super-admins
45 #exception_tracker.send_email_recipients =
@@ -9,6 +9,7 b' Release Notes'
9 9 .. toctree::
10 10 :maxdepth: 1
11 11
12 release-notes-4.20.0.rst
12 13 release-notes-4.19.3.rst
13 14 release-notes-4.19.2.rst
14 15 release-notes-4.19.1.rst
@@ -51,9 +51,12 b''
51 51 "<%= dirs.js.src %>/plugins/jquery.pjax.js",
52 52 "<%= dirs.js.src %>/plugins/jquery.dataTables.js",
53 53 "<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
54 "<%= dirs.js.src %>/plugins/within_viewport.js",
54 55 "<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js",
55 56 "<%= dirs.js.src %>/plugins/jquery.autocomplete.js",
56 57 "<%= dirs.js.src %>/plugins/jquery.debounce.js",
58 "<%= dirs.js.src %>/plugins/jquery.scrollstop.js",
59 "<%= dirs.js.src %>/plugins/jquery.within-viewport.js",
57 60 "<%= dirs.js.node_modules %>/mark.js/dist/jquery.mark.min.js",
58 61 "<%= dirs.js.src %>/plugins/jquery.timeago.js",
59 62 "<%= dirs.js.src %>/plugins/jquery.timeago-extension.js",
@@ -1819,7 +1819,7 b' self: super: {'
1819 1819 };
1820 1820 };
1821 1821 "rhodecode-enterprise-ce" = super.buildPythonPackage {
1822 name = "rhodecode-enterprise-ce-4.19.3";
1822 name = "rhodecode-enterprise-ce-4.20.0";
1823 1823 buildInputs = [
1824 1824 self."pytest"
1825 1825 self."py"
@@ -10,6 +10,8 b' vcsserver_config_http = rhodecode/tests/'
10 10 addopts =
11 11 --pdbcls=IPython.terminal.debugger:TerminalPdb
12 12 --strict-markers
13 --capture=no
14 --show-capture=no
13 15
14 16 markers =
15 17 vcs_operations: Mark tests depending on a running RhodeCode instance.
@@ -1,1 +1,1 b''
1 4.19.3 No newline at end of file
1 4.20.0 No newline at end of file
@@ -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__ = 107 # defines current db version for migrations
51 __dbversion__ = 108 # defines current db version for migrations
52 52 __platform__ = platform.system()
53 53 __license__ = 'AGPLv3, and Commercial License'
54 54 __author__ = 'RhodeCode GmbH'
@@ -88,7 +88,8 b' class TestApi(object):'
88 88 response = api_call(self.app, params)
89 89 expected = 'No such method: comment. ' \
90 90 'Similar methods: changeset_comment, comment_pull_request, ' \
91 'get_pull_request_comments, comment_commit, get_repo_comments'
91 'get_pull_request_comments, comment_commit, edit_comment, ' \
92 'get_comment, get_repo_comments'
92 93 assert_error(id_, expected, given=response.body)
93 94
94 95 def test_api_disabled_user(self, request):
@@ -21,7 +21,7 b''
21 21 import pytest
22 22
23 23 from rhodecode.model.comment import CommentsModel
24 from rhodecode.model.db import UserLog, User
24 from rhodecode.model.db import UserLog, User, ChangesetComment
25 25 from rhodecode.model.pull_request import PullRequestModel
26 26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 27 from rhodecode.api.tests.utils import (
@@ -218,8 +218,20 b' class TestCommentPullRequest(object):'
218 218 assert_error(id_, expected, given=response.body)
219 219
220 220 @pytest.mark.backends("git", "hg")
221 def test_api_comment_pull_request_non_admin_with_userid_error(
222 self, pr_util):
221 def test_api_comment_pull_request_non_admin_with_userid_error(self, pr_util):
222 pull_request = pr_util.create_pull_request()
223 id_, params = build_data(
224 self.apikey_regular, 'comment_pull_request',
225 repoid=pull_request.target_repo.repo_name,
226 pullrequestid=pull_request.pull_request_id,
227 userid=TEST_USER_ADMIN_LOGIN)
228 response = api_call(self.app, params)
229
230 expected = 'userid is not the same as your user'
231 assert_error(id_, expected, given=response.body)
232
233 @pytest.mark.backends("git", "hg")
234 def test_api_comment_pull_request_non_admin_with_userid_error(self, pr_util):
223 235 pull_request = pr_util.create_pull_request()
224 236 id_, params = build_data(
225 237 self.apikey_regular, 'comment_pull_request',
@@ -244,3 +256,135 b' class TestCommentPullRequest(object):'
244 256
245 257 expected = 'Invalid commit_id `XXX` for this pull request.'
246 258 assert_error(id_, expected, given=response.body)
259
260 @pytest.mark.backends("git", "hg")
261 def test_api_edit_comment(self, pr_util):
262 pull_request = pr_util.create_pull_request()
263
264 id_, params = build_data(
265 self.apikey,
266 'comment_pull_request',
267 repoid=pull_request.target_repo.repo_name,
268 pullrequestid=pull_request.pull_request_id,
269 message='test message',
270 )
271 response = api_call(self.app, params)
272 json_response = response.json
273 comment_id = json_response['result']['comment_id']
274
275 message_after_edit = 'just message'
276 id_, params = build_data(
277 self.apikey,
278 'edit_comment',
279 comment_id=comment_id,
280 message=message_after_edit,
281 version=0,
282 )
283 response = api_call(self.app, params)
284 json_response = response.json
285 assert json_response['result']['version'] == 1
286
287 text_form_db = ChangesetComment.get(comment_id).text
288 assert message_after_edit == text_form_db
289
290 @pytest.mark.backends("git", "hg")
291 def test_api_edit_comment_wrong_version(self, pr_util):
292 pull_request = pr_util.create_pull_request()
293
294 id_, params = build_data(
295 self.apikey, 'comment_pull_request',
296 repoid=pull_request.target_repo.repo_name,
297 pullrequestid=pull_request.pull_request_id,
298 message='test message')
299 response = api_call(self.app, params)
300 json_response = response.json
301 comment_id = json_response['result']['comment_id']
302
303 message_after_edit = 'just message'
304 id_, params = build_data(
305 self.apikey_regular,
306 'edit_comment',
307 comment_id=comment_id,
308 message=message_after_edit,
309 version=1,
310 )
311 response = api_call(self.app, params)
312 expected = 'comment ({}) version ({}) mismatch'.format(comment_id, 1)
313 assert_error(id_, expected, given=response.body)
314
315 @pytest.mark.backends("git", "hg")
316 def test_api_edit_comment_wrong_version(self, pr_util):
317 pull_request = pr_util.create_pull_request()
318
319 id_, params = build_data(
320 self.apikey, 'comment_pull_request',
321 repoid=pull_request.target_repo.repo_name,
322 pullrequestid=pull_request.pull_request_id,
323 message='test message')
324 response = api_call(self.app, params)
325 json_response = response.json
326 comment_id = json_response['result']['comment_id']
327
328 id_, params = build_data(
329 self.apikey,
330 'edit_comment',
331 comment_id=comment_id,
332 message='',
333 version=0,
334 )
335 response = api_call(self.app, params)
336 expected = "comment ({}) can't be changed with empty string".format(comment_id, 1)
337 assert_error(id_, expected, given=response.body)
338
339 @pytest.mark.backends("git", "hg")
340 def test_api_edit_comment_wrong_user_set_by_non_admin(self, pr_util):
341 pull_request = pr_util.create_pull_request()
342 pull_request_id = pull_request.pull_request_id
343 id_, params = build_data(
344 self.apikey,
345 'comment_pull_request',
346 repoid=pull_request.target_repo.repo_name,
347 pullrequestid=pull_request_id,
348 message='test message'
349 )
350 response = api_call(self.app, params)
351 json_response = response.json
352 comment_id = json_response['result']['comment_id']
353
354 id_, params = build_data(
355 self.apikey_regular,
356 'edit_comment',
357 comment_id=comment_id,
358 message='just message',
359 version=0,
360 userid=TEST_USER_ADMIN_LOGIN
361 )
362 response = api_call(self.app, params)
363 expected = 'userid is not the same as your user'
364 assert_error(id_, expected, given=response.body)
365
366 @pytest.mark.backends("git", "hg")
367 def test_api_edit_comment_wrong_user_with_permissions_to_edit_comment(self, pr_util):
368 pull_request = pr_util.create_pull_request()
369 pull_request_id = pull_request.pull_request_id
370 id_, params = build_data(
371 self.apikey,
372 'comment_pull_request',
373 repoid=pull_request.target_repo.repo_name,
374 pullrequestid=pull_request_id,
375 message='test message'
376 )
377 response = api_call(self.app, params)
378 json_response = response.json
379 comment_id = json_response['result']['comment_id']
380
381 id_, params = build_data(
382 self.apikey_regular,
383 'edit_comment',
384 comment_id=comment_id,
385 message='just message',
386 version=0,
387 )
388 response = api_call(self.app, params)
389 expected = "you don't have access to edit this comment"
390 assert_error(id_, expected, given=response.body)
@@ -233,8 +233,8 b' class TestCreateRepoGroup(object):'
233 233
234 234 expected = {
235 235 'repo_group':
236 'Parent repository group `{}` does not exist'.format(
237 repo_group_name)}
236 u"You do not have the permissions to store "
237 u"repository groups inside repository group `{}`".format(repo_group_name)}
238 238 try:
239 239 assert_error(id_, expected, given=response.body)
240 240 finally:
@@ -37,8 +37,10 b' class TestGetMethod(object):'
37 37 id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
38 38 response = api_call(self.app, params)
39 39
40 expected = ['changeset_comment', 'comment_pull_request',
41 'get_pull_request_comments', 'comment_commit', 'get_repo_comments']
40 expected = [
41 'changeset_comment', 'comment_pull_request', 'get_pull_request_comments',
42 'comment_commit', 'edit_comment', 'get_comment', 'get_repo_comments'
43 ]
42 44 assert_ok(id_, expected, given=response.body)
43 45
44 46 def test_get_methods_on_single_match(self):
@@ -61,6 +61,7 b' class TestGetPullRequestComments(object)'
61 61 'comment_type': 'note',
62 62 'comment_resolved_by': None,
63 63 'pull_request_version': None,
64 'comment_last_version': 0,
64 65 'comment_commit_id': None,
65 66 'comment_pull_request_id': pull_request.pull_request_id
66 67 }
@@ -42,26 +42,27 b' def make_repo_comments_factory(request):'
42 42 comments = []
43 43
44 44 # general
45 CommentsModel().create(
45 comment = CommentsModel().create(
46 46 text='General Comment', repo=repo, user=user, commit_id=commit_id,
47 47 comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
48 comments.append(comment)
48 49
49 50 # inline
50 CommentsModel().create(
51 comment = CommentsModel().create(
51 52 text='Inline Comment', repo=repo, user=user, commit_id=commit_id,
52 53 f_path=file_0, line_no='n1',
53 54 comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
55 comments.append(comment)
54 56
55 57 # todo
56 CommentsModel().create(
58 comment = CommentsModel().create(
57 59 text='INLINE TODO Comment', repo=repo, user=user, commit_id=commit_id,
58 60 f_path=file_0, line_no='n1',
59 61 comment_type=ChangesetComment.COMMENT_TYPE_TODO, send_email=False)
62 comments.append(comment)
60 63
61 @request.addfinalizer
62 def cleanup():
63 for comment in comments:
64 Session().delete(comment)
64 return comments
65
65 66 return Make()
66 67
67 68
@@ -108,3 +109,34 b' class TestGetRepo(object):'
108 109 id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params)
109 110 response = api_call(self.app, params)
110 111 assert_error(id_, expected, given=response.body)
112
113 def test_api_get_comment(self, make_repo_comments_factory, backend_hg):
114 commits = [{'message': 'A'}, {'message': 'B'}]
115 repo = backend_hg.create_repo(commits=commits)
116
117 comments = make_repo_comments_factory.make_comments(repo)
118 comment_ids = [x.comment_id for x in comments]
119 Session().commit()
120
121 for comment_id in comment_ids:
122 id_, params = build_data(self.apikey, 'get_comment',
123 **{'comment_id': comment_id})
124 response = api_call(self.app, params)
125 result = assert_call_ok(id_, given=response.body)
126 assert result['comment_id'] == comment_id
127
128 def test_api_get_comment_no_access(self, make_repo_comments_factory, backend_hg, user_util):
129 commits = [{'message': 'A'}, {'message': 'B'}]
130 repo = backend_hg.create_repo(commits=commits)
131 comments = make_repo_comments_factory.make_comments(repo)
132 comment_id = comments[0].comment_id
133
134 test_user = user_util.create_user()
135 user_util.grant_user_permission_to_repo(repo, test_user, 'repository.none')
136
137 id_, params = build_data(test_user.api_key, 'get_comment',
138 **{'comment_id': comment_id})
139 response = api_call(self.app, params)
140 assert_error(id_,
141 expected='comment `{}` does not exist'.format(comment_id),
142 given=response.body)
@@ -21,7 +21,6 b''
21 21
22 22 import logging
23 23
24 from rhodecode import events
25 24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 25 from rhodecode.api.utils import (
27 26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
@@ -36,8 +35,7 b' from rhodecode.model.db import Session, '
36 35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 36 from rhodecode.model.settings import SettingsModel
38 37 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
38 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
41 39
42 40 log = logging.getLogger(__name__)
43 41
@@ -292,10 +290,11 b' def merge_pull_request('
292 290 else:
293 291 repo = pull_request.target_repo
294 292 auth_user = apiuser
293
295 294 if not isinstance(userid, Optional):
296 if (has_superadmin_permission(apiuser) or
297 HasRepoPermissionAnyApi('repository.admin')(
298 user=apiuser, repo_name=repo.repo_name)):
295 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
296 user=apiuser, repo_name=repo.repo_name)
297 if has_superadmin_permission(apiuser) or is_repo_admin:
299 298 apiuser = get_user_or_error(userid)
300 299 auth_user = apiuser.AuthUser()
301 300 else:
@@ -379,6 +378,7 b' def get_pull_request_comments('
379 378 },
380 379 "comment_text": "Example text",
381 380 "comment_type": null,
381 "comment_last_version: 0,
382 382 "pull_request_version": null,
383 383 "comment_commit_id": None,
384 384 "comment_pull_request_id": <pull_request_id>
@@ -510,9 +510,9 b' def comment_pull_request('
510 510
511 511 auth_user = apiuser
512 512 if not isinstance(userid, Optional):
513 if (has_superadmin_permission(apiuser) or
514 HasRepoPermissionAnyApi('repository.admin')(
515 user=apiuser, repo_name=repo.repo_name)):
513 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
514 user=apiuser, repo_name=repo.repo_name)
515 if has_superadmin_permission(apiuser) or is_repo_admin:
516 516 apiuser = get_user_or_error(userid)
517 517 auth_user = apiuser.AuthUser()
518 518 else:
@@ -979,10 +979,10 b' def close_pull_request('
979 979 else:
980 980 repo = pull_request.target_repo
981 981
982 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
983 user=apiuser, repo_name=repo.repo_name)
982 984 if not isinstance(userid, Optional):
983 if (has_superadmin_permission(apiuser) or
984 HasRepoPermissionAnyApi('repository.admin')(
985 user=apiuser, repo_name=repo.repo_name)):
985 if has_superadmin_permission(apiuser) or is_repo_admin:
986 986 apiuser = get_user_or_error(userid)
987 987 else:
988 988 raise JSONRPCError('userid is not the same as your user')
@@ -31,11 +31,15 b' from rhodecode.api.utils import ('
31 31 validate_set_owner_permissions)
32 32 from rhodecode.lib import audit_logger, rc_cache
33 33 from rhodecode.lib import repo_maintenance
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
34 from rhodecode.lib.auth import (
35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
36 HasRepoPermissionAnyApi)
35 37 from rhodecode.lib.celerylib.utils import get_task_id
36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int, safe_unicode
38 from rhodecode.lib.utils2 import (
39 str2bool, time_to_datetime, safe_str, safe_int, safe_unicode)
37 40 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 from rhodecode.lib.exceptions import (
42 StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
39 43 from rhodecode.lib.vcs import RepositoryError
40 44 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
41 45 from rhodecode.model.changeset_status import ChangesetStatusModel
@@ -44,6 +48,7 b' from rhodecode.model.db import ('
44 48 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
45 49 ChangesetComment)
46 50 from rhodecode.model.permission import PermissionModel
51 from rhodecode.model.pull_request import PullRequestModel
47 52 from rhodecode.model.repo import RepoModel
48 53 from rhodecode.model.scm import ScmModel, RepoList
49 54 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
@@ -1719,7 +1724,8 b' def get_repo_comments(request, apiuser, '
1719 1724 "comment_resolved_by": null,
1720 1725 "comment_status": [],
1721 1726 "comment_text": "This file needs a header",
1722 "comment_type": "todo"
1727 "comment_type": "todo",
1728 "comment_last_version: 0
1723 1729 }
1724 1730 ],
1725 1731 "error" : null
@@ -1752,6 +1758,157 b' def get_repo_comments(request, apiuser, '
1752 1758
1753 1759
1754 1760 @jsonrpc_method()
1761 def get_comment(request, apiuser, comment_id):
1762 """
1763 Get single comment from repository or pull_request
1764
1765 :param apiuser: This is filled automatically from the |authtoken|.
1766 :type apiuser: AuthUser
1767 :param comment_id: comment id found in the URL of comment
1768 :type comment_id: str or int
1769
1770 Example error output:
1771
1772 .. code-block:: bash
1773
1774 {
1775 "id" : <id_given_in_input>,
1776 "result" : {
1777 "comment_author": <USER_DETAILS>,
1778 "comment_created_on": "2017-02-01T14:38:16.309",
1779 "comment_f_path": "file.txt",
1780 "comment_id": 282,
1781 "comment_lineno": "n1",
1782 "comment_resolved_by": null,
1783 "comment_status": [],
1784 "comment_text": "This file needs a header",
1785 "comment_type": "todo",
1786 "comment_last_version: 0
1787 },
1788 "error" : null
1789 }
1790
1791 """
1792
1793 comment = ChangesetComment.get(comment_id)
1794 if not comment:
1795 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1796
1797 perms = ('repository.read', 'repository.write', 'repository.admin')
1798 has_comment_perm = HasRepoPermissionAnyApi(*perms)\
1799 (user=apiuser, repo_name=comment.repo.repo_name)
1800
1801 if not has_comment_perm:
1802 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1803
1804 return comment
1805
1806
1807 @jsonrpc_method()
1808 def edit_comment(request, apiuser, message, comment_id, version,
1809 userid=Optional(OAttr('apiuser'))):
1810 """
1811 Edit comment on the pull request or commit,
1812 specified by the `comment_id` and version. Initially version should be 0
1813
1814 :param apiuser: This is filled automatically from the |authtoken|.
1815 :type apiuser: AuthUser
1816 :param comment_id: Specify the comment_id for editing
1817 :type comment_id: int
1818 :param version: version of the comment that will be created, starts from 0
1819 :type version: int
1820 :param message: The text content of the comment.
1821 :type message: str
1822 :param userid: Comment on the pull request as this user
1823 :type userid: Optional(str or int)
1824
1825 Example output:
1826
1827 .. code-block:: bash
1828
1829 id : <id_given_in_input>
1830 result : {
1831 "comment": "<comment data>",
1832 "version": "<Integer>",
1833 },
1834 error : null
1835 """
1836
1837 auth_user = apiuser
1838 comment = ChangesetComment.get(comment_id)
1839 if not comment:
1840 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1841
1842 is_super_admin = has_superadmin_permission(apiuser)
1843 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1844 (user=apiuser, repo_name=comment.repo.repo_name)
1845
1846 if not isinstance(userid, Optional):
1847 if is_super_admin or is_repo_admin:
1848 apiuser = get_user_or_error(userid)
1849 auth_user = apiuser.AuthUser()
1850 else:
1851 raise JSONRPCError('userid is not the same as your user')
1852
1853 comment_author = comment.author.user_id == auth_user.user_id
1854 if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1855 raise JSONRPCError("you don't have access to edit this comment")
1856
1857 try:
1858 comment_history = CommentsModel().edit(
1859 comment_id=comment_id,
1860 text=message,
1861 auth_user=auth_user,
1862 version=version,
1863 )
1864 Session().commit()
1865 except CommentVersionMismatch:
1866 raise JSONRPCError(
1867 'comment ({}) version ({}) mismatch'.format(comment_id, version)
1868 )
1869 if not comment_history and not message:
1870 raise JSONRPCError(
1871 "comment ({}) can't be changed with empty string".format(comment_id)
1872 )
1873
1874 if comment.pull_request:
1875 pull_request = comment.pull_request
1876 PullRequestModel().trigger_pull_request_hook(
1877 pull_request, apiuser, 'comment_edit',
1878 data={'comment': comment})
1879 else:
1880 db_repo = comment.repo
1881 commit_id = comment.revision
1882 commit = db_repo.get_commit(commit_id)
1883 CommentsModel().trigger_commit_comment_hook(
1884 db_repo, apiuser, 'edit',
1885 data={'comment': comment, 'commit': commit})
1886
1887 data = {
1888 'comment': comment,
1889 'version': comment_history.version if comment_history else None,
1890 }
1891 return data
1892
1893
1894 # TODO(marcink): write this with all required logic for deleting a comments in PR or commits
1895 # @jsonrpc_method()
1896 # def delete_comment(request, apiuser, comment_id):
1897 # auth_user = apiuser
1898 #
1899 # comment = ChangesetComment.get(comment_id)
1900 # if not comment:
1901 # raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1902 #
1903 # is_super_admin = has_superadmin_permission(apiuser)
1904 # is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1905 # (user=apiuser, repo_name=comment.repo.repo_name)
1906 #
1907 # comment_author = comment.author.user_id == auth_user.user_id
1908 # if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1909 # raise JSONRPCError("you don't have access to edit this comment")
1910
1911 @jsonrpc_method()
1755 1912 def grant_user_permission(request, apiuser, repoid, userid, perm):
1756 1913 """
1757 1914 Grant permissions for the specified user on the given repository,
@@ -69,6 +69,7 b' class AdminRepoGroupsView(BaseAppView, D'
69 69 c.repo_groups = RepoGroup.groups_choices(
70 70 groups=groups_with_admin_rights,
71 71 show_empty_group=allow_empty_group)
72 c.personal_repo_group = self._rhodecode_user.personal_repo_group
72 73
73 74 def _can_create_repo_group(self, parent_group_id=None):
74 75 is_admin = HasPermissionAny('hg.admin')('group create controller')
@@ -261,15 +262,28 b' class AdminRepoGroupsView(BaseAppView, D'
261 262
262 263 # perm check for admin, create_group perm or admin of parent_group
263 264 parent_group_id = safe_int(self.request.GET.get('parent_group'))
265 _gr = RepoGroup.get(parent_group_id)
264 266 if not self._can_create_repo_group(parent_group_id):
265 267 raise HTTPForbidden()
266 268
267 269 self._load_form_data(c)
268 270
269 271 defaults = {} # Future proof for default of repo group
272
273 parent_group_choice = '-1'
274 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
275 parent_group_choice = self._rhodecode_user.personal_repo_group
276
277 if parent_group_id and _gr:
278 if parent_group_id in [x[0] for x in c.repo_groups]:
279 parent_group_choice = safe_unicode(parent_group_id)
280
281 defaults.update({'group_parent_id': parent_group_choice})
282
270 283 data = render(
271 284 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
272 285 self._get_template_context(c), self.request)
286
273 287 html = formencode.htmlfill.render(
274 288 data,
275 289 defaults=defaults,
@@ -169,8 +169,8 b' class AdminReposView(BaseAppView, DataGr'
169 169 c = self.load_default_context()
170 170
171 171 new_repo = self.request.GET.get('repo', '')
172 parent_group = safe_int(self.request.GET.get('parent_group'))
173 _gr = RepoGroup.get(parent_group)
172 parent_group_id = safe_int(self.request.GET.get('parent_group'))
173 _gr = RepoGroup.get(parent_group_id)
174 174
175 175 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
176 176 # you're not super admin nor have global create permissions,
@@ -196,9 +196,9 b' class AdminReposView(BaseAppView, DataGr'
196 196 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
197 197 parent_group_choice = self._rhodecode_user.personal_repo_group
198 198
199 if parent_group and _gr:
200 if parent_group in [x[0] for x in c.repo_groups]:
201 parent_group_choice = safe_unicode(parent_group)
199 if parent_group_id and _gr:
200 if parent_group_id in [x[0] for x in c.repo_groups]:
201 parent_group_choice = safe_unicode(parent_group_id)
202 202
203 203 defaults.update({'repo_group': parent_group_choice})
204 204
@@ -47,6 +47,7 b' from rhodecode.model.db import RhodeCode'
47 47 from rhodecode.model.forms import (ApplicationSettingsForm,
48 48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 49 LabsSettingsForm, IssueTrackerPatternsForm)
50 from rhodecode.model.permission import PermissionModel
50 51 from rhodecode.model.repo_group import RepoGroupModel
51 52
52 53 from rhodecode.model.scm import ScmModel
@@ -253,8 +254,7 b' class AdminSettingsView(BaseAppView):'
253 254 c.active = 'mapping'
254 255 rm_obsolete = self.request.POST.get('destroy', False)
255 256 invalidate_cache = self.request.POST.get('invalidate', False)
256 log.debug(
257 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
257 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
258 258
259 259 if invalidate_cache:
260 260 log.debug('invalidating all repositories cache')
@@ -263,6 +263,8 b' class AdminSettingsView(BaseAppView):'
263 263
264 264 filesystem_repos = ScmModel().repo_scan()
265 265 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
266 PermissionModel().trigger_permission_flush()
267
266 268 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
267 269 h.flash(_('Repositories successfully '
268 270 'rescanned added: %s ; removed: %s') %
@@ -576,8 +578,7 b' class AdminSettingsView(BaseAppView):'
576 578 'user': self._rhodecode_db_user
577 579 }
578 580
579 (subject, headers, email_body,
580 email_body_plaintext) = EmailNotificationModel().render_email(
581 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
581 582 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
582 583
583 584 recipients = [test_email] if test_email else None
@@ -376,8 +376,7 b' users: description edit fixes'
376 376 }
377 377
378 378 template_type = email_id.split('+')[0]
379 (c.subject, c.headers, c.email_body,
380 c.email_body_plaintext) = EmailNotificationModel().render_email(
379 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
381 380 template_type, **email_kwargs.get(email_id, {}))
382 381
383 382 test_email = self.request.GET.get('email')
@@ -302,7 +302,7 b' class TestGistsController(TestController'
302 302 assert_response = response.assert_response()
303 303 assert_response.element_equals_to(
304 304 'div.rc-user span.user',
305 '<a href="/_profiles/test_admin">test_admin</a></span>')
305 '<a href="/_profiles/test_admin">test_admin</a>')
306 306
307 307 response.mustcontain('gist-desc')
308 308
@@ -328,7 +328,7 b' class TestGistsController(TestController'
328 328 assert_response = response.assert_response()
329 329 assert_response.element_equals_to(
330 330 'div.rc-user span.user',
331 '<a href="/_profiles/test_admin">test_admin</a></span>')
331 '<a href="/_profiles/test_admin">test_admin</a>')
332 332 response.mustcontain('gist-desc')
333 333
334 334 def test_show_as_raw(self, create_gist):
@@ -79,6 +79,10 b' def includeme(config):'
79 79 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
80 80
81 81 config.add_route(
82 name='repo_commit_comment_history_view',
83 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_history_id}/history_view', repo_route=True)
84
85 config.add_route(
82 86 name='repo_commit_comment_attachment_upload',
83 87 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
84 88
@@ -86,6 +90,10 b' def includeme(config):'
86 90 name='repo_commit_comment_delete',
87 91 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
88 92
93 config.add_route(
94 name='repo_commit_comment_edit',
95 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
96
89 97 # still working url for backward compat.
90 98 config.add_route(
91 99 name='repo_commit_raw_deprecated',
@@ -328,6 +336,11 b' def includeme(config):'
328 336 repo_route=True)
329 337
330 338 config.add_route(
339 name='pullrequest_comment_edit',
340 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
341 repo_route=True, repo_accepted_types=['hg', 'git'])
342
343 config.add_route(
331 344 name='pullrequest_comment_delete',
332 345 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
333 346 repo_route=True, repo_accepted_types=['hg', 'git'])
@@ -35,6 +35,7 b' def route_path(name, params=None, **kwar'
35 35 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
36 36 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
37 37 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
38 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
38 39 }[name].format(**kwargs)
39 40
40 41 if params:
@@ -268,6 +269,164 b' class TestRepoCommitCommentsView(TestCon'
268 269 repo_name=backend.repo_name, commit_id=commit_id))
269 270 assert_comment_links(response, 0, 0)
270 271
272 def test_edit(self, backend):
273 self.log_user()
274 commit_id = backend.repo.get_commit('300').raw_id
275 text = u'CommentOnCommit'
276
277 params = {'text': text, 'csrf_token': self.csrf_token}
278 self.app.post(
279 route_path(
280 'repo_commit_comment_create',
281 repo_name=backend.repo_name, commit_id=commit_id),
282 params=params)
283
284 comments = ChangesetComment.query().all()
285 assert len(comments) == 1
286 comment_id = comments[0].comment_id
287 test_text = 'test_text'
288 self.app.post(
289 route_path(
290 'repo_commit_comment_edit',
291 repo_name=backend.repo_name,
292 commit_id=commit_id,
293 comment_id=comment_id,
294 ),
295 params={
296 'csrf_token': self.csrf_token,
297 'text': test_text,
298 'version': '0',
299 })
300
301 text_form_db = ChangesetComment.query().filter(
302 ChangesetComment.comment_id == comment_id).first().text
303 assert test_text == text_form_db
304
305 def test_edit_without_change(self, backend):
306 self.log_user()
307 commit_id = backend.repo.get_commit('300').raw_id
308 text = u'CommentOnCommit'
309
310 params = {'text': text, 'csrf_token': self.csrf_token}
311 self.app.post(
312 route_path(
313 'repo_commit_comment_create',
314 repo_name=backend.repo_name, commit_id=commit_id),
315 params=params)
316
317 comments = ChangesetComment.query().all()
318 assert len(comments) == 1
319 comment_id = comments[0].comment_id
320
321 response = self.app.post(
322 route_path(
323 'repo_commit_comment_edit',
324 repo_name=backend.repo_name,
325 commit_id=commit_id,
326 comment_id=comment_id,
327 ),
328 params={
329 'csrf_token': self.csrf_token,
330 'text': text,
331 'version': '0',
332 },
333 status=404,
334 )
335 assert response.status_int == 404
336
337 def test_edit_try_edit_already_edited(self, backend):
338 self.log_user()
339 commit_id = backend.repo.get_commit('300').raw_id
340 text = u'CommentOnCommit'
341
342 params = {'text': text, 'csrf_token': self.csrf_token}
343 self.app.post(
344 route_path(
345 'repo_commit_comment_create',
346 repo_name=backend.repo_name, commit_id=commit_id
347 ),
348 params=params,
349 )
350
351 comments = ChangesetComment.query().all()
352 assert len(comments) == 1
353 comment_id = comments[0].comment_id
354 test_text = 'test_text'
355 self.app.post(
356 route_path(
357 'repo_commit_comment_edit',
358 repo_name=backend.repo_name,
359 commit_id=commit_id,
360 comment_id=comment_id,
361 ),
362 params={
363 'csrf_token': self.csrf_token,
364 'text': test_text,
365 'version': '0',
366 }
367 )
368 test_text_v2 = 'test_v2'
369 response = self.app.post(
370 route_path(
371 'repo_commit_comment_edit',
372 repo_name=backend.repo_name,
373 commit_id=commit_id,
374 comment_id=comment_id,
375 ),
376 params={
377 'csrf_token': self.csrf_token,
378 'text': test_text_v2,
379 'version': '0',
380 },
381 status=409,
382 )
383 assert response.status_int == 409
384
385 text_form_db = ChangesetComment.query().filter(
386 ChangesetComment.comment_id == comment_id).first().text
387
388 assert test_text == text_form_db
389 assert test_text_v2 != text_form_db
390
391 def test_edit_forbidden_for_immutable_comments(self, backend):
392 self.log_user()
393 commit_id = backend.repo.get_commit('300').raw_id
394 text = u'CommentOnCommit'
395
396 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
397 self.app.post(
398 route_path(
399 'repo_commit_comment_create',
400 repo_name=backend.repo_name,
401 commit_id=commit_id,
402 ),
403 params=params
404 )
405
406 comments = ChangesetComment.query().all()
407 assert len(comments) == 1
408 comment_id = comments[0].comment_id
409
410 comment = ChangesetComment.get(comment_id)
411 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
412 Session().add(comment)
413 Session().commit()
414
415 response = self.app.post(
416 route_path(
417 'repo_commit_comment_edit',
418 repo_name=backend.repo_name,
419 commit_id=commit_id,
420 comment_id=comment_id,
421 ),
422 params={
423 'csrf_token': self.csrf_token,
424 'text': 'test_text',
425 },
426 status=403,
427 )
428 assert response.status_int == 403
429
271 430 def test_delete_forbidden_for_immutable_comments(self, backend):
272 431 self.log_user()
273 432 commit_id = backend.repo.get_commit('300').raw_id
@@ -30,6 +30,7 b' from rhodecode.model.db import ('
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.model.pull_request import PullRequestModel
32 32 from rhodecode.model.user import UserModel
33 from rhodecode.model.comment import CommentsModel
33 34 from rhodecode.tests import (
34 35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 36
@@ -54,6 +55,7 b' def route_path(name, params=None, **kwar'
54 55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
57 59 }[name].format(**kwargs)
58 60
59 61 if params:
@@ -114,6 +116,223 b' class TestPullrequestsView(object):'
114 116 if range_diff == "1":
115 117 response.mustcontain('Turn off: Show the diff as commit range')
116 118
119 def test_show_versions_of_pr(self, backend, csrf_token):
120 commits = [
121 {'message': 'initial-commit',
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
123
124 {'message': 'commit-1',
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
126 # Above is the initial version of PR that changes a single line
127
128 # from now on we'll add 3x commit adding a nother line on each step
129 {'message': 'commit-2',
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
131
132 {'message': 'commit-3',
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134
135 {'message': 'commit-4',
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 ]
138
139 commit_ids = backend.create_master_repo(commits)
140 target = backend.create_repo(heads=['initial-commit'])
141 source = backend.create_repo(heads=['commit-1'])
142 source_repo_name = source.repo_name
143 target_repo_name = target.repo_name
144
145 target_ref = 'branch:{branch}:{commit_id}'.format(
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 source_ref = 'branch:{branch}:{commit_id}'.format(
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149
150 response = self.app.post(
151 route_path('pullrequest_create', repo_name=source.repo_name),
152 [
153 ('source_repo', source.repo_name),
154 ('source_ref', source_ref),
155 ('target_repo', target.repo_name),
156 ('target_ref', target_ref),
157 ('common_ancestor', commit_ids['initial-commit']),
158 ('pullrequest_title', 'Title'),
159 ('pullrequest_desc', 'Description'),
160 ('description_renderer', 'markdown'),
161 ('__start__', 'review_members:sequence'),
162 ('__start__', 'reviewer:mapping'),
163 ('user_id', '1'),
164 ('__start__', 'reasons:sequence'),
165 ('reason', 'Some reason'),
166 ('__end__', 'reasons:sequence'),
167 ('__start__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
169 ('mandatory', 'False'),
170 ('__end__', 'reviewer:mapping'),
171 ('__end__', 'review_members:sequence'),
172 ('__start__', 'revisions:sequence'),
173 ('revisions', commit_ids['commit-1']),
174 ('__end__', 'revisions:sequence'),
175 ('user', ''),
176 ('csrf_token', csrf_token),
177 ],
178 status=302)
179
180 location = response.headers['Location']
181
182 pull_request_id = location.rsplit('/', 1)[1]
183 assert pull_request_id != 'new'
184 pull_request = PullRequest.get(int(pull_request_id))
185
186 pull_request_id = pull_request.pull_request_id
187
188 # Show initial version of PR
189 response = self.app.get(
190 route_path('pullrequest_show',
191 repo_name=target_repo_name,
192 pull_request_id=pull_request_id))
193
194 response.mustcontain('commit-1')
195 response.mustcontain(no=['commit-2'])
196 response.mustcontain(no=['commit-3'])
197 response.mustcontain(no=['commit-4'])
198
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 response.mustcontain(no=['LINE3'])
201 response.mustcontain(no=['LINE4'])
202 response.mustcontain(no=['LINE5'])
203
204 # update PR #1
205 source_repo = Repository.get_by_repo_name(source_repo_name)
206 backend.pull_heads(source_repo, heads=['commit-2'])
207 response = self.app.post(
208 route_path('pullrequest_update',
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
211
212 # update PR #2
213 source_repo = Repository.get_by_repo_name(source_repo_name)
214 backend.pull_heads(source_repo, heads=['commit-3'])
215 response = self.app.post(
216 route_path('pullrequest_update',
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
219
220 # update PR #3
221 source_repo = Repository.get_by_repo_name(source_repo_name)
222 backend.pull_heads(source_repo, heads=['commit-4'])
223 response = self.app.post(
224 route_path('pullrequest_update',
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
227
228 # Show final version !
229 response = self.app.get(
230 route_path('pullrequest_show',
231 repo_name=target_repo_name,
232 pull_request_id=pull_request_id))
233
234 # 3 updates, and the latest == 4
235 response.mustcontain('4 versions available for this pull request')
236 response.mustcontain(no=['rhodecode diff rendering error'])
237
238 # initial show must have 3 commits, and 3 adds
239 response.mustcontain('commit-1')
240 response.mustcontain('commit-2')
241 response.mustcontain('commit-3')
242 response.mustcontain('commit-4')
243
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248
249 # fetch versions
250 pr = PullRequest.get(pull_request_id)
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 assert len(versions) == 3
253
254 # show v1,v2,v3,v4
255 def cb_line(text):
256 return 'cb-addition"></span><span>{}</span>'.format(text)
257
258 def cb_context(text):
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 '</span><span>{}</span></span>'.format(text)
261
262 commit_tests = {
263 # in response, not in response
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 }
269 diff_tests = {
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 }
275 for idx, ver in enumerate(versions, 1):
276
277 response = self.app.get(
278 route_path('pullrequest_show',
279 repo_name=target_repo_name,
280 pull_request_id=pull_request_id,
281 params={'version': ver}))
282
283 response.mustcontain(no=['rhodecode diff rendering error'])
284 response.mustcontain('Showing changes at v{}'.format(idx))
285
286 yes, no = commit_tests[idx]
287 for y in yes:
288 response.mustcontain(y)
289 for n in no:
290 response.mustcontain(no=n)
291
292 yes, no = diff_tests[idx]
293 for y in yes:
294 response.mustcontain(cb_line(y))
295 for n in no:
296 response.mustcontain(no=n)
297
298 # show diff between versions
299 diff_compare_tests = {
300 1: (['LINE3'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 }
304 for idx, ver in enumerate(versions, 1):
305 adds, context = diff_compare_tests[idx]
306
307 to_ver = ver+1
308 if idx == 3:
309 to_ver = 'latest'
310
311 response = self.app.get(
312 route_path('pullrequest_show',
313 repo_name=target_repo_name,
314 pull_request_id=pull_request_id,
315 params={'from_version': versions[0], 'version': to_ver}))
316
317 response.mustcontain(no=['rhodecode diff rendering error'])
318
319 for a in adds:
320 response.mustcontain(cb_line(a))
321 for c in context:
322 response.mustcontain(cb_context(c))
323
324 # test version v2 -> v3
325 response = self.app.get(
326 route_path('pullrequest_show',
327 repo_name=target_repo_name,
328 pull_request_id=pull_request_id,
329 params={'from_version': versions[1], 'version': versions[2]}))
330
331 response.mustcontain(cb_context('LINE1'))
332 response.mustcontain(cb_context('LINE2'))
333 response.mustcontain(cb_context('LINE3'))
334 response.mustcontain(cb_line('LINE4'))
335
117 336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
118 337 # Logout
119 338 response = self.app.post(
@@ -355,6 +574,222 b' class TestPullrequestsView(object):'
355 574 pull_request.source_repo, pull_request=pull_request)
356 575 assert status == ChangesetStatus.STATUS_REJECTED
357 576
577 def test_comment_and_close_pull_request_try_edit_comment(
578 self, pr_util, csrf_token, xhr_header
579 ):
580 pull_request = pr_util.create_pull_request()
581 pull_request_id = pull_request.pull_request_id
582 target_scm = pull_request.target_repo.scm_instance()
583 target_scm_name = target_scm.name
584
585 response = self.app.post(
586 route_path(
587 'pullrequest_comment_create',
588 repo_name=target_scm_name,
589 pull_request_id=pull_request_id,
590 ),
591 params={
592 'close_pull_request': 'true',
593 'csrf_token': csrf_token,
594 },
595 extra_environ=xhr_header)
596
597 assert response.json
598
599 pull_request = PullRequest.get(pull_request_id)
600 target_scm = pull_request.target_repo.scm_instance()
601 target_scm_name = target_scm.name
602 assert pull_request.is_closed()
603
604 # check only the latest status, not the review status
605 status = ChangesetStatusModel().get_status(
606 pull_request.source_repo, pull_request=pull_request)
607 assert status == ChangesetStatus.STATUS_REJECTED
608
609 comment_id = response.json.get('comment_id', None)
610 test_text = 'test'
611 response = self.app.post(
612 route_path(
613 'pullrequest_comment_edit',
614 repo_name=target_scm_name,
615 pull_request_id=pull_request_id,
616 comment_id=comment_id,
617 ),
618 extra_environ=xhr_header,
619 params={
620 'csrf_token': csrf_token,
621 'text': test_text,
622 },
623 status=403,
624 )
625 assert response.status_int == 403
626
627 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
628 pull_request = pr_util.create_pull_request()
629 target_scm = pull_request.target_repo.scm_instance()
630 target_scm_name = target_scm.name
631
632 response = self.app.post(
633 route_path(
634 'pullrequest_comment_create',
635 repo_name=target_scm_name,
636 pull_request_id=pull_request.pull_request_id),
637 params={
638 'csrf_token': csrf_token,
639 'text': 'init',
640 },
641 extra_environ=xhr_header,
642 )
643 assert response.json
644
645 comment_id = response.json.get('comment_id', None)
646 assert comment_id
647 test_text = 'test'
648 self.app.post(
649 route_path(
650 'pullrequest_comment_edit',
651 repo_name=target_scm_name,
652 pull_request_id=pull_request.pull_request_id,
653 comment_id=comment_id,
654 ),
655 extra_environ=xhr_header,
656 params={
657 'csrf_token': csrf_token,
658 'text': test_text,
659 'version': '0',
660 },
661
662 )
663 text_form_db = ChangesetComment.query().filter(
664 ChangesetComment.comment_id == comment_id).first().text
665 assert test_text == text_form_db
666
667 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
668 pull_request = pr_util.create_pull_request()
669 target_scm = pull_request.target_repo.scm_instance()
670 target_scm_name = target_scm.name
671
672 response = self.app.post(
673 route_path(
674 'pullrequest_comment_create',
675 repo_name=target_scm_name,
676 pull_request_id=pull_request.pull_request_id),
677 params={
678 'csrf_token': csrf_token,
679 'text': 'init',
680 },
681 extra_environ=xhr_header,
682 )
683 assert response.json
684
685 comment_id = response.json.get('comment_id', None)
686 assert comment_id
687 test_text = 'init'
688 response = self.app.post(
689 route_path(
690 'pullrequest_comment_edit',
691 repo_name=target_scm_name,
692 pull_request_id=pull_request.pull_request_id,
693 comment_id=comment_id,
694 ),
695 extra_environ=xhr_header,
696 params={
697 'csrf_token': csrf_token,
698 'text': test_text,
699 'version': '0',
700 },
701 status=404,
702
703 )
704 assert response.status_int == 404
705
706 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
707 pull_request = pr_util.create_pull_request()
708 target_scm = pull_request.target_repo.scm_instance()
709 target_scm_name = target_scm.name
710
711 response = self.app.post(
712 route_path(
713 'pullrequest_comment_create',
714 repo_name=target_scm_name,
715 pull_request_id=pull_request.pull_request_id),
716 params={
717 'csrf_token': csrf_token,
718 'text': 'init',
719 },
720 extra_environ=xhr_header,
721 )
722 assert response.json
723 comment_id = response.json.get('comment_id', None)
724 assert comment_id
725
726 test_text = 'test'
727 self.app.post(
728 route_path(
729 'pullrequest_comment_edit',
730 repo_name=target_scm_name,
731 pull_request_id=pull_request.pull_request_id,
732 comment_id=comment_id,
733 ),
734 extra_environ=xhr_header,
735 params={
736 'csrf_token': csrf_token,
737 'text': test_text,
738 'version': '0',
739 },
740
741 )
742 test_text_v2 = 'test_v2'
743 response = self.app.post(
744 route_path(
745 'pullrequest_comment_edit',
746 repo_name=target_scm_name,
747 pull_request_id=pull_request.pull_request_id,
748 comment_id=comment_id,
749 ),
750 extra_environ=xhr_header,
751 params={
752 'csrf_token': csrf_token,
753 'text': test_text_v2,
754 'version': '0',
755 },
756 status=409,
757 )
758 assert response.status_int == 409
759
760 text_form_db = ChangesetComment.query().filter(
761 ChangesetComment.comment_id == comment_id).first().text
762
763 assert test_text == text_form_db
764 assert test_text_v2 != text_form_db
765
766 def test_comment_and_comment_edit_permissions_forbidden(
767 self, autologin_regular_user, user_regular, user_admin, pr_util,
768 csrf_token, xhr_header):
769 pull_request = pr_util.create_pull_request(
770 author=user_admin.username, enable_notifications=False)
771 comment = CommentsModel().create(
772 text='test',
773 repo=pull_request.target_repo.scm_instance().name,
774 user=user_admin,
775 pull_request=pull_request,
776 )
777 response = self.app.post(
778 route_path(
779 'pullrequest_comment_edit',
780 repo_name=pull_request.target_repo.scm_instance().name,
781 pull_request_id=pull_request.pull_request_id,
782 comment_id=comment.comment_id,
783 ),
784 extra_environ=xhr_header,
785 params={
786 'csrf_token': csrf_token,
787 'text': 'test_text',
788 },
789 status=403,
790 )
791 assert response.status_int == 403
792
358 793 def test_create_pull_request(self, backend, csrf_token):
359 794 commits = [
360 795 {'message': 'ancestor'},
@@ -20,9 +20,9 b''
20 20
21 21
22 22 import logging
23 import collections
24 23
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
@@ -39,13 +39,14 b' from rhodecode.lib.compat import Ordered'
39 39 from rhodecode.lib.diffs import (
40 40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 43 import rhodecode.lib.helpers as h
44 44 from rhodecode.lib.utils2 import safe_unicode, str2bool
45 45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 46 from rhodecode.lib.vcs.exceptions import (
47 47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 51 from rhodecode.model.comment import CommentsModel
51 52 from rhodecode.model.meta import Session
@@ -431,6 +432,34 b' class RepoCommitsView(RepoAppView):'
431 432 'repository.read', 'repository.write', 'repository.admin')
432 433 @CSRFRequired()
433 434 @view_config(
435 route_name='repo_commit_comment_history_view', request_method='POST',
436 renderer='string', xhr=True)
437 def repo_commit_comment_history_view(self):
438 c = self.load_default_context()
439
440 comment_history_id = self.request.matchdict['comment_history_id']
441 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
442 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
443
444 if is_repo_comment:
445 c.comment_history = comment_history
446
447 rendered_comment = render(
448 'rhodecode:templates/changeset/comment_history.mako',
449 self._get_template_context(c)
450 , self.request)
451 return rendered_comment
452 else:
453 log.warning('No permissions for user %s to show comment_history_id: %s',
454 self._rhodecode_db_user, comment_history_id)
455 raise HTTPNotFound()
456
457 @LoginRequired()
458 @NotAnonymous()
459 @HasRepoPermissionAnyDecorator(
460 'repository.read', 'repository.write', 'repository.admin')
461 @CSRFRequired()
462 @view_config(
434 463 route_name='repo_commit_comment_attachment_upload', request_method='POST',
435 464 renderer='json_ext', xhr=True)
436 465 def repo_commit_comment_attachment_upload(self):
@@ -545,7 +574,7 b' class RepoCommitsView(RepoAppView):'
545 574 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
546 575 super_admin = h.HasPermissionAny('hg.admin')()
547 576 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
548 is_repo_comment = comment.repo.repo_name == self.db_repo_name
577 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
549 578 comment_repo_admin = is_repo_admin and is_repo_comment
550 579
551 580 if super_admin or comment_owner or comment_repo_admin:
@@ -558,6 +587,90 b' class RepoCommitsView(RepoAppView):'
558 587 raise HTTPNotFound()
559 588
560 589 @LoginRequired()
590 @NotAnonymous()
591 @HasRepoPermissionAnyDecorator(
592 'repository.read', 'repository.write', 'repository.admin')
593 @CSRFRequired()
594 @view_config(
595 route_name='repo_commit_comment_edit', request_method='POST',
596 renderer='json_ext')
597 def repo_commit_comment_edit(self):
598 self.load_default_context()
599
600 comment_id = self.request.matchdict['comment_id']
601 comment = ChangesetComment.get_or_404(comment_id)
602
603 if comment.immutable:
604 # don't allow deleting comments that are immutable
605 raise HTTPForbidden()
606
607 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
608 super_admin = h.HasPermissionAny('hg.admin')()
609 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
610 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
611 comment_repo_admin = is_repo_admin and is_repo_comment
612
613 if super_admin or comment_owner or comment_repo_admin:
614 text = self.request.POST.get('text')
615 version = self.request.POST.get('version')
616 if text == comment.text:
617 log.warning(
618 'Comment(repo): '
619 'Trying to create new version '
620 'with the same comment body {}'.format(
621 comment_id,
622 )
623 )
624 raise HTTPNotFound()
625
626 if version.isdigit():
627 version = int(version)
628 else:
629 log.warning(
630 'Comment(repo): Wrong version type {} {} '
631 'for comment {}'.format(
632 version,
633 type(version),
634 comment_id,
635 )
636 )
637 raise HTTPNotFound()
638
639 try:
640 comment_history = CommentsModel().edit(
641 comment_id=comment_id,
642 text=text,
643 auth_user=self._rhodecode_user,
644 version=version,
645 )
646 except CommentVersionMismatch:
647 raise HTTPConflict()
648
649 if not comment_history:
650 raise HTTPNotFound()
651
652 commit_id = self.request.matchdict['commit_id']
653 commit = self.db_repo.get_commit(commit_id)
654 CommentsModel().trigger_commit_comment_hook(
655 self.db_repo, self._rhodecode_user, 'edit',
656 data={'comment': comment, 'commit': commit})
657
658 Session().commit()
659 return {
660 'comment_history_id': comment_history.comment_history_id,
661 'comment_id': comment.comment_id,
662 'comment_version': comment_history.version,
663 'comment_author_username': comment_history.author.username,
664 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
665 'comment_created_on': h.age_component(comment_history.created_on,
666 time_is_local=True),
667 }
668 else:
669 log.warning('No permissions for user %s to edit comment_id: %s',
670 self._rhodecode_db_user, comment_id)
671 raise HTTPNotFound()
672
673 @LoginRequired()
561 674 @HasRepoPermissionAnyDecorator(
562 675 'repository.read', 'repository.write', 'repository.admin')
563 676 @view_config(
@@ -125,7 +125,7 b' class RepoFilesView(RepoAppView):'
125 125 self.db_repo_name, branch_name)
126 126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 127 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 h.escape(branch_name), rule)
128 h.escape(branch_name), h.escape(rule))
129 129 h.flash(message, 'warning')
130 130
131 131 if json_mode:
@@ -25,7 +25,7 b' import formencode'
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
@@ -34,6 +34,7 b' from rhodecode.apps._base import RepoApp'
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 38 from rhodecode.lib.ext_json import json
38 39 from rhodecode.lib.auth import (
39 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
@@ -213,9 +214,12 b' class RepoPullRequestsView(RepoAppView, '
213 214 ancestor_commit,
214 215 source_ref_id, target_ref_id,
215 216 target_commit, source_commit, diff_limit, file_limit,
216 fulldiff, hide_whitespace_changes, diff_context):
217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
217 218
219 if use_ancestor:
220 # we might want to not use it for versions
218 221 target_ref_id = ancestor_commit.raw_id
222
219 223 vcs_diff = PullRequestModel().get_diff(
220 224 source_repo, source_ref_id, target_ref_id,
221 225 hide_whitespace_changes, diff_context)
@@ -568,7 +572,6 b' class RepoPullRequestsView(RepoAppView, '
568 572 c.commit_ranges.append(comm)
569 573
570 574 c.missing_requirements = missing_requirements
571
572 575 c.ancestor_commit = ancestor_commit
573 576 c.statuses = source_repo.statuses(
574 577 [x.raw_id for x in c.commit_ranges])
@@ -593,6 +596,10 b' class RepoPullRequestsView(RepoAppView, '
593 596 else:
594 597 c.inline_comments = display_inline_comments
595 598
599 use_ancestor = True
600 if from_version_normalized != version_normalized:
601 use_ancestor = False
602
596 603 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
597 604 if not force_recache and has_proper_diff_cache:
598 605 c.diffset = cached_diff['diff']
@@ -604,7 +611,10 b' class RepoPullRequestsView(RepoAppView, '
604 611 source_ref_id, target_ref_id,
605 612 target_commit, source_commit,
606 613 diff_limit, file_limit, c.fulldiff,
607 hide_whitespace_changes, diff_context)
614 hide_whitespace_changes, diff_context,
615 use_ancestor=use_ancestor
616 )
617
608 618 # save cached diff
609 619 if caching_enabled:
610 620 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
@@ -1524,3 +1534,104 b' class RepoPullRequestsView(RepoAppView, '
1524 1534 log.warning('No permissions for user %s to delete comment_id: %s',
1525 1535 self._rhodecode_db_user, comment_id)
1526 1536 raise HTTPNotFound()
1537
1538 @LoginRequired()
1539 @NotAnonymous()
1540 @HasRepoPermissionAnyDecorator(
1541 'repository.read', 'repository.write', 'repository.admin')
1542 @CSRFRequired()
1543 @view_config(
1544 route_name='pullrequest_comment_edit', request_method='POST',
1545 renderer='json_ext')
1546 def pull_request_comment_edit(self):
1547 self.load_default_context()
1548
1549 pull_request = PullRequest.get_or_404(
1550 self.request.matchdict['pull_request_id']
1551 )
1552 comment = ChangesetComment.get_or_404(
1553 self.request.matchdict['comment_id']
1554 )
1555 comment_id = comment.comment_id
1556
1557 if comment.immutable:
1558 # don't allow deleting comments that are immutable
1559 raise HTTPForbidden()
1560
1561 if pull_request.is_closed():
1562 log.debug('comment: forbidden because pull request is closed')
1563 raise HTTPForbidden()
1564
1565 if not comment:
1566 log.debug('Comment with id:%s not found, skipping', comment_id)
1567 # comment already deleted in another call probably
1568 return True
1569
1570 if comment.pull_request.is_closed():
1571 # don't allow deleting comments on closed pull request
1572 raise HTTPForbidden()
1573
1574 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1575 super_admin = h.HasPermissionAny('hg.admin')()
1576 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1577 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1578 comment_repo_admin = is_repo_admin and is_repo_comment
1579
1580 if super_admin or comment_owner or comment_repo_admin:
1581 text = self.request.POST.get('text')
1582 version = self.request.POST.get('version')
1583 if text == comment.text:
1584 log.warning(
1585 'Comment(PR): '
1586 'Trying to create new version '
1587 'with the same comment body {}'.format(
1588 comment_id,
1589 )
1590 )
1591 raise HTTPNotFound()
1592
1593 if version.isdigit():
1594 version = int(version)
1595 else:
1596 log.warning(
1597 'Comment(PR): Wrong version type {} {} '
1598 'for comment {}'.format(
1599 version,
1600 type(version),
1601 comment_id,
1602 )
1603 )
1604 raise HTTPNotFound()
1605
1606 try:
1607 comment_history = CommentsModel().edit(
1608 comment_id=comment_id,
1609 text=text,
1610 auth_user=self._rhodecode_user,
1611 version=version,
1612 )
1613 except CommentVersionMismatch:
1614 raise HTTPConflict()
1615
1616 if not comment_history:
1617 raise HTTPNotFound()
1618
1619 Session().commit()
1620
1621 PullRequestModel().trigger_pull_request_hook(
1622 pull_request, self._rhodecode_user, 'comment_edit',
1623 data={'comment': comment})
1624
1625 return {
1626 'comment_history_id': comment_history.comment_history_id,
1627 'comment_id': comment.comment_id,
1628 'comment_version': comment_history.version,
1629 'comment_author_username': comment_history.author.username,
1630 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1631 'comment_created_on': h.age_component(comment_history.created_on,
1632 time_is_local=True),
1633 }
1634 else:
1635 log.warning('No permissions for user %s to edit comment_id: %s',
1636 self._rhodecode_db_user, comment_id)
1637 raise HTTPNotFound()
@@ -743,7 +743,7 b' def authenticate(username, password, env'
743 743 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
744 744 plugin.get_id(), plugin_cache_active, cache_ttl)
745 745
746 user_id = user.user_id if user else None
746 user_id = user.user_id if user else 'no-user'
747 747 # don't cache for empty users
748 748 plugin_cache_active = plugin_cache_active and user_id
749 749 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
@@ -26,6 +26,7 b' from .hooks import ('
26 26 _pre_create_user_hook,
27 27 _create_user_hook,
28 28 _comment_commit_repo_hook,
29 _comment_edit_commit_repo_hook,
29 30 _delete_repo_hook,
30 31 _delete_user_hook,
31 32 _pre_push_hook,
@@ -35,6 +36,7 b' from .hooks import ('
35 36 _create_pull_request_hook,
36 37 _review_pull_request_hook,
37 38 _comment_pull_request_hook,
39 _comment_edit_pull_request_hook,
38 40 _update_pull_request_hook,
39 41 _merge_pull_request_hook,
40 42 _close_pull_request_hook,
@@ -43,6 +45,7 b' from .hooks import ('
43 45 # set as module attributes, we use those to call hooks. *do not change this*
44 46 CREATE_REPO_HOOK = _create_repo_hook
45 47 COMMENT_COMMIT_REPO_HOOK = _comment_commit_repo_hook
48 COMMENT_EDIT_COMMIT_REPO_HOOK = _comment_edit_commit_repo_hook
46 49 CREATE_REPO_GROUP_HOOK = _create_repo_group_hook
47 50 PRE_CREATE_USER_HOOK = _pre_create_user_hook
48 51 CREATE_USER_HOOK = _create_user_hook
@@ -55,6 +58,7 b' PULL_HOOK = _pull_hook'
55 58 CREATE_PULL_REQUEST = _create_pull_request_hook
56 59 REVIEW_PULL_REQUEST = _review_pull_request_hook
57 60 COMMENT_PULL_REQUEST = _comment_pull_request_hook
61 COMMENT_EDIT_PULL_REQUEST = _comment_edit_pull_request_hook
58 62 UPDATE_PULL_REQUEST = _update_pull_request_hook
59 63 MERGE_PULL_REQUEST = _merge_pull_request_hook
60 64 CLOSE_PULL_REQUEST = _close_pull_request_hook
@@ -1,5 +1,6 b''
1 # This code allows override the integrations templates.
2 # Put this into the __init__.py file of rcextensions to override the templates
1 # Below code examples allows override the integrations templates, or email titles.
2 # Append selected parts at the end of the __init__.py file of rcextensions directory
3 # to override the templates
3 4
4 5
5 6 # EMAIL Integration
@@ -185,3 +186,18 b' message:'
185 186 ```
186 187
187 188 ''')
189
190
191 # Example to modify emails default title
192 from rhodecode.model import notification
193
194 notification.EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = '{updating_user} updated pull request. !{pr_id}: "{pr_title}"'
195 notification.EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = '{user} requested a pull request review. !{pr_id}: "{pr_title}"'
196
197 notification.EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"'
198 notification.EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = '{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"'
199 notification.EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"'
200
201 notification.EMAIL_COMMENT_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on commit `{commit_id}`'
202 notification.EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = '{mention_prefix}[status: {status}] {user} left a {comment_type} on commit `{commit_id}`'
203 notification.EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`'
@@ -83,6 +83,33 b' def _comment_commit_repo_hook(*args, **k'
83 83
84 84
85 85 @has_kwargs({
86 'repo_name': '',
87 'repo_type': '',
88 'description': '',
89 'private': '',
90 'created_on': '',
91 'enable_downloads': '',
92 'repo_id': '',
93 'user_id': '',
94 'enable_statistics': '',
95 'clone_uri': '',
96 'fork_id': '',
97 'group_id': '',
98 'created_by': '',
99 'repository': '',
100 'comment': '',
101 'commit': ''
102 })
103 def _comment_edit_commit_repo_hook(*args, **kwargs):
104 """
105 POST CREATE REPOSITORY COMMENT ON COMMIT HOOK. This function will be executed after
106 a comment is made on this repository commit.
107
108 """
109 return HookResponse(0, '')
110
111
112 @has_kwargs({
86 113 'group_name': '',
87 114 'group_parent_id': '',
88 115 'group_description': '',
@@ -408,6 +435,38 b' def _comment_pull_request_hook(*args, **'
408 435 'scm': 'type of version control "git", "hg", "svn"',
409 436 'username': 'username of actor who triggered this event',
410 437 'ip': 'ip address of actor who triggered this hook',
438
439 'action': '',
440 'repository': 'repository name',
441 'pull_request_id': '',
442 'url': '',
443 'title': '',
444 'description': '',
445 'status': '',
446 'comment': '',
447 'created_on': '',
448 'updated_on': '',
449 'commit_ids': '',
450 'review_status': '',
451 'mergeable': '',
452 'source': '',
453 'target': '',
454 'author': '',
455 'reviewers': '',
456 })
457 def _comment_edit_pull_request_hook(*args, **kwargs):
458 """
459 This hook will be executed after comment is made on a pull request
460 """
461 return HookResponse(0, '')
462
463
464 @has_kwargs({
465 'server_url': 'url of instance that triggered this hook',
466 'config': 'path to .ini config used',
467 'scm': 'type of version control "git", "hg", "svn"',
468 'username': 'username of actor who triggered this event',
469 'ip': 'ip address of actor who triggered this hook',
411 470 'action': '',
412 471 'repository': 'repository name',
413 472 'pull_request_id': '',
@@ -74,7 +74,7 b' link_config = ['
74 74 },
75 75 {
76 76 "name": "rst_help",
77 "target": "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
77 "target": "http://docutils.sourceforge.io/docs/user/rst/quickref.html",
78 78 "external_target": "https://docutils.sourceforge.io/docs/user/rst/quickref.html",
79 79 },
80 80 {
@@ -53,7 +53,8 b' from rhodecode.events.user import ( # p'
53 53 )
54 54
55 55 from rhodecode.events.repo import ( # pragma: no cover
56 RepoEvent, RepoCommitCommentEvent,
56 RepoEvent,
57 RepoCommitCommentEvent, RepoCommitCommentEditEvent,
57 58 RepoPreCreateEvent, RepoCreateEvent,
58 59 RepoPreDeleteEvent, RepoDeleteEvent,
59 60 RepoPrePushEvent, RepoPushEvent,
@@ -72,8 +73,8 b' from rhodecode.events.pullrequest import'
72 73 PullRequestCreateEvent,
73 74 PullRequestUpdateEvent,
74 75 PullRequestCommentEvent,
76 PullRequestCommentEditEvent,
75 77 PullRequestReviewEvent,
76 78 PullRequestMergeEvent,
77 79 PullRequestCloseEvent,
78 PullRequestCommentEvent,
79 80 )
@@ -19,8 +19,7 b''
19 19 import logging
20 20
21 21 from rhodecode.translation import lazy_ugettext
22 from rhodecode.events.repo import (
23 RepoEvent, _commits_as_dict, _issues_as_dict)
22 from rhodecode.events.repo import (RepoEvent, _commits_as_dict, _issues_as_dict)
24 23
25 24 log = logging.getLogger(__name__)
26 25
@@ -155,6 +154,7 b' class PullRequestCommentEvent(PullReques'
155 154 'type': self.comment.comment_type,
156 155 'file': self.comment.f_path,
157 156 'line': self.comment.line_no,
157 'version': self.comment.last_version,
158 158 'url': CommentsModel().get_url(
159 159 self.comment, request=self.request),
160 160 'permalink_url': CommentsModel().get_url(
@@ -162,3 +162,42 b' class PullRequestCommentEvent(PullReques'
162 162 }
163 163 })
164 164 return data
165
166
167 class PullRequestCommentEditEvent(PullRequestEvent):
168 """
169 An instance of this class is emitted as an :term:`event` after a pull
170 request comment is edited.
171 """
172 name = 'pullrequest-comment-edit'
173 display_name = lazy_ugettext('pullrequest comment edited')
174 description = lazy_ugettext('Event triggered after a comment was edited on a code '
175 'in the pull request')
176
177 def __init__(self, pullrequest, comment):
178 super(PullRequestCommentEditEvent, self).__init__(pullrequest)
179 self.comment = comment
180
181 def as_dict(self):
182 from rhodecode.model.comment import CommentsModel
183 data = super(PullRequestCommentEditEvent, self).as_dict()
184
185 status = None
186 if self.comment.status_change:
187 status = self.comment.status_change[0].status
188
189 data.update({
190 'comment': {
191 'status': status,
192 'text': self.comment.text,
193 'type': self.comment.comment_type,
194 'file': self.comment.f_path,
195 'line': self.comment.line_no,
196 'version': self.comment.last_version,
197 'url': CommentsModel().get_url(
198 self.comment, request=self.request),
199 'permalink_url': CommentsModel().get_url(
200 self.comment, request=self.request, permalink=True),
201 }
202 })
203 return data
@@ -211,6 +211,42 b' class RepoCommitCommentEvent(RepoEvent):'
211 211 'comment_type': self.comment.comment_type,
212 212 'comment_f_path': self.comment.f_path,
213 213 'comment_line_no': self.comment.line_no,
214 'comment_version': self.comment.last_version,
215 }
216 return data
217
218
219 class RepoCommitCommentEditEvent(RepoEvent):
220 """
221 An instance of this class is emitted as an :term:`event` after a comment is edited
222 on repository commit.
223 """
224
225 name = 'repo-commit-edit-comment'
226 display_name = lazy_ugettext('repository commit edit comment')
227 description = lazy_ugettext('Event triggered after a comment was edited '
228 'on commit inside a repository')
229
230 def __init__(self, repo, commit, comment):
231 super(RepoCommitCommentEditEvent, self).__init__(repo)
232 self.commit = commit
233 self.comment = comment
234
235 def as_dict(self):
236 data = super(RepoCommitCommentEditEvent, self).as_dict()
237 data['commit'] = {
238 'commit_id': self.commit.raw_id,
239 'commit_message': self.commit.message,
240 'commit_branch': self.commit.branch,
241 }
242
243 data['comment'] = {
244 'comment_id': self.comment.comment_id,
245 'comment_text': self.comment.text,
246 'comment_type': self.comment.comment_type,
247 'comment_f_path': self.comment.f_path,
248 'comment_line_no': self.comment.line_no,
249 'comment_version': self.comment.last_version,
214 250 }
215 251 return data
216 252
@@ -331,6 +331,26 b' class WebhookDataHandler(CommitParsingDa'
331 331
332 332 return [(url, self.headers, data)]
333 333
334 def repo_commit_comment_edit_handler(self, event, data):
335 url = self.get_base_parsed_template(data)
336 log.debug('register %s call(%s) to url %s', self.name, event, url)
337 comment_vars = [
338 ('commit_comment_id', data['comment']['comment_id']),
339 ('commit_comment_text', data['comment']['comment_text']),
340 ('commit_comment_type', data['comment']['comment_type']),
341
342 ('commit_comment_f_path', data['comment']['comment_f_path']),
343 ('commit_comment_line_no', data['comment']['comment_line_no']),
344
345 ('commit_comment_commit_id', data['commit']['commit_id']),
346 ('commit_comment_commit_branch', data['commit']['commit_branch']),
347 ('commit_comment_commit_message', data['commit']['commit_message']),
348 ]
349 for k, v in comment_vars:
350 url = UrlTmpl(url).safe_substitute(**{k: v})
351
352 return [(url, self.headers, data)]
353
334 354 def repo_create_event_handler(self, event, data):
335 355 url = self.get_base_parsed_template(data)
336 356 log.debug('register %s call(%s) to url %s', self.name, event, url)
@@ -360,6 +380,8 b' class WebhookDataHandler(CommitParsingDa'
360 380 return self.repo_create_event_handler(event, data)
361 381 elif isinstance(event, events.RepoCommitCommentEvent):
362 382 return self.repo_commit_comment_handler(event, data)
383 elif isinstance(event, events.RepoCommitCommentEditEvent):
384 return self.repo_commit_comment_edit_handler(event, data)
363 385 elif isinstance(event, events.PullRequestEvent):
364 386 return self.pull_request_event_handler(event, data)
365 387 else:
@@ -133,6 +133,8 b' class HipchatIntegrationType(Integration'
133 133
134 134 if isinstance(event, events.PullRequestCommentEvent):
135 135 text = self.format_pull_request_comment_event(event, data)
136 elif isinstance(event, events.PullRequestCommentEditEvent):
137 text = self.format_pull_request_comment_event(event, data)
136 138 elif isinstance(event, events.PullRequestReviewEvent):
137 139 text = self.format_pull_request_review_event(event, data)
138 140 elif isinstance(event, events.PullRequestEvent):
@@ -157,6 +157,9 b' class SlackIntegrationType(IntegrationTy'
157 157 if isinstance(event, events.PullRequestCommentEvent):
158 158 (title, text, fields, overrides) \
159 159 = self.format_pull_request_comment_event(event, data)
160 elif isinstance(event, events.PullRequestCommentEditEvent):
161 (title, text, fields, overrides) \
162 = self.format_pull_request_comment_event(event, data)
160 163 elif isinstance(event, events.PullRequestReviewEvent):
161 164 title, text = self.format_pull_request_review_event(event, data)
162 165 elif isinstance(event, events.PullRequestEvent):
@@ -144,11 +144,13 b' class WebhookIntegrationType(Integration'
144 144 events.PullRequestMergeEvent,
145 145 events.PullRequestUpdateEvent,
146 146 events.PullRequestCommentEvent,
147 events.PullRequestCommentEditEvent,
147 148 events.PullRequestReviewEvent,
148 149 events.PullRequestCreateEvent,
149 150 events.RepoPushEvent,
150 151 events.RepoCreateEvent,
151 152 events.RepoCommitCommentEvent,
153 events.RepoCommitCommentEditEvent,
152 154 ]
153 155
154 156 def settings_schema(self):
@@ -82,6 +82,7 b' ACTIONS_V1 = {'
82 82 'repo.pull_request.merge': '',
83 83 'repo.pull_request.vote': '',
84 84 'repo.pull_request.comment.create': '',
85 'repo.pull_request.comment.edit': '',
85 86 'repo.pull_request.comment.delete': '',
86 87
87 88 'repo.pull_request.reviewer.add': '',
@@ -90,6 +91,7 b' ACTIONS_V1 = {'
90 91 'repo.commit.strip': {'commit_id': ''},
91 92 'repo.commit.comment.create': {'data': {}},
92 93 'repo.commit.comment.delete': {'data': {}},
94 'repo.commit.comment.edit': {'data': {}},
93 95 'repo.commit.vote': '',
94 96
95 97 'repo.artifact.add': '',
@@ -367,8 +367,7 b' class PermOriginDict(dict):'
367 367 self.perm_origin_stack = collections.OrderedDict()
368 368
369 369 def __setitem__(self, key, (perm, origin, obj_id)):
370 self.perm_origin_stack.setdefault(key, []).append(
371 (perm, origin, obj_id))
370 self.perm_origin_stack.setdefault(key, []).append((perm, origin, obj_id))
372 371 dict.__setitem__(self, key, perm)
373 372
374 373
@@ -441,7 +440,7 b' class PermissionCalculator(object):'
441 440
442 441 def calculate(self):
443 442 if self.user_is_admin and not self.calculate_super_admin_as_user:
444 return self._calculate_admin_permissions()
443 return self._calculate_super_admin_permissions()
445 444
446 445 self._calculate_global_default_permissions()
447 446 self._calculate_global_permissions()
@@ -452,9 +451,9 b' class PermissionCalculator(object):'
452 451 self._calculate_user_group_permissions()
453 452 return self._permission_structure()
454 453
455 def _calculate_admin_permissions(self):
454 def _calculate_super_admin_permissions(self):
456 455 """
457 admin user have all default rights for repositories
456 super-admin user have all default rights for repositories
458 457 and groups set to admin
459 458 """
460 459 self.permissions_global.add('hg.admin')
@@ -774,6 +773,7 b' class PermissionCalculator(object):'
774 773 for perm in user_repo_perms:
775 774 r_k = perm.UserRepoToPerm.repository.repo_name
776 775 obj_id = perm.UserRepoToPerm.repository.repo_id
776 archived = perm.UserRepoToPerm.repository.archived
777 777 p = perm.Permission.permission_name
778 778 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
779 779
@@ -795,6 +795,15 b' class PermissionCalculator(object):'
795 795 o = PermOrigin.SUPER_ADMIN
796 796 self.permissions_repositories[r_k] = p, o, obj_id
797 797
798 # finally in case of archived repositories, we downgrade higher
799 # permissions to read
800 if archived:
801 current_perm = self.permissions_repositories[r_k]
802 if current_perm in ['repository.write', 'repository.admin']:
803 p = 'repository.read'
804 o = PermOrigin.ARCHIVED
805 self.permissions_repositories[r_k] = p, o, obj_id
806
798 807 def _calculate_repository_branch_permissions(self):
799 808 # user group for repositories permissions
800 809 user_repo_branch_perms_from_user_group = Permission\
@@ -384,7 +384,8 b' def attach_context_attributes(context, r'
384 384 session_attrs = {
385 385 # defaults
386 386 "clone_url_format": "http",
387 "diffmode": "sideside"
387 "diffmode": "sideside",
388 "license_fingerprint": request.session.get('license_fingerprint')
388 389 }
389 390
390 391 if not is_api:
@@ -61,6 +61,8 b' markdown_tags = ['
61 61 "img",
62 62 "a",
63 63 "input",
64 "details",
65 "summary"
64 66 ]
65 67
66 68 markdown_attrs = {
@@ -29,18 +29,20 b' import time'
29 29 from pyramid import compat
30 30 from pyramid_mailer.mailer import Mailer
31 31 from pyramid_mailer.message import Message
32 from email.utils import formatdate
32 33
33 34 import rhodecode
34 35 from rhodecode.lib import audit_logger
35 36 from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask
36 from rhodecode.lib.hooks_base import log_create_repository
37 from rhodecode.lib import hooks_base
37 38 from rhodecode.lib.utils2 import safe_int, str2bool
38 39 from rhodecode.model.db import (
39 40 Session, IntegrityError, true, Repository, RepoGroup, User)
40 41
41 42
42 43 @async_task(ignore_result=True, base=RequestContextTask)
43 def send_email(recipients, subject, body='', html_body='', email_config=None):
44 def send_email(recipients, subject, body='', html_body='', email_config=None,
45 extra_headers=None):
44 46 """
45 47 Sends an email with defined parameters from the .ini files.
46 48
@@ -50,6 +52,7 b' def send_email(recipients, subject, body'
50 52 :param body: body of the mail
51 53 :param html_body: html version of body
52 54 :param email_config: specify custom configuration for mailer
55 :param extra_headers: specify custom headers
53 56 """
54 57 log = get_logger(send_email)
55 58
@@ -108,13 +111,23 b' def send_email(recipients, subject, body'
108 111 # sendmail_template='',
109 112 )
110 113
114 if extra_headers is None:
115 extra_headers = {}
116
117 extra_headers.setdefault('Date', formatdate(time.time()))
118
119 if 'thread_ids' in extra_headers:
120 thread_ids = extra_headers.pop('thread_ids')
121 extra_headers['References'] = ' '.join('<{}>'.format(t) for t in thread_ids)
122
111 123 try:
112 124 mailer = Mailer(**email_conf)
113 125
114 126 message = Message(subject=subject,
115 127 sender=email_conf['default_sender'],
116 128 recipients=recipients,
117 body=body, html=html_body)
129 body=body, html=html_body,
130 extra_headers=extra_headers)
118 131 mailer.send_immediately(message)
119 132
120 133 except Exception:
@@ -187,7 +200,7 b' def create_repo(form_data, cur_user):'
187 200 clone_uri=clone_uri,
188 201 )
189 202 repo = Repository.get_by_repo_name(repo_name_full)
190 log_create_repository(created_by=owner.username, **repo.get_dict())
203 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
191 204
192 205 # update repo commit caches initially
193 206 repo.update_commit_cache()
@@ -273,7 +286,7 b' def create_repo_fork(form_data, cur_user'
273 286 clone_uri=source_repo_path,
274 287 )
275 288 repo = Repository.get_by_repo_name(repo_name_full)
276 log_create_repository(created_by=owner.username, **repo.get_dict())
289 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
277 290
278 291 # update repo commit caches initially
279 292 config = repo._config
@@ -540,10 +540,11 b' class DiffSet(object):'
540 540 })
541 541
542 542 file_chunks = patch['chunks'][1:]
543 for hunk in file_chunks:
543 for i, hunk in enumerate(file_chunks, 1):
544 544 hunkbit = self.parse_hunk(hunk, source_file, target_file)
545 545 hunkbit.source_file_path = source_file_path
546 546 hunkbit.target_file_path = target_file_path
547 hunkbit.index = i
547 548 filediff.hunks.append(hunkbit)
548 549
549 550 # Simulate hunk on OPS type line which doesn't really contain any diff
@@ -143,8 +143,7 b' def send_exc_email(request, exc_id, exc_'
143 143 'exc_traceback': read_exception(exc_id, prefix=None),
144 144 }
145 145
146 (subject, headers, email_body,
147 email_body_plaintext) = EmailNotificationModel().render_email(
146 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
148 147 EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs)
149 148
150 149 run_task(tasks.send_email, recipients, subject,
@@ -177,3 +177,7 b' class ArtifactMetadataDuplicate(ValueErr'
177 177
178 178 class ArtifactMetadataBadValueType(ValueError):
179 179 pass
180
181
182 class CommentVersionMismatch(ValueError):
183 pass
@@ -24,6 +24,7 b' Helper functions'
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 import base64
27 28
28 29 import os
29 30 import random
@@ -52,7 +53,7 b' from pygments.lexers import ('
52 53 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 54
54 55 from pyramid.threadlocal import get_current_request
55
56 from tempita import looper
56 57 from webhelpers2.html import literal, HTML, escape
57 58 from webhelpers2.html._autolink import _auto_link_urls
58 59 from webhelpers2.html.tools import (
@@ -85,10 +86,11 b' from rhodecode.lib.utils2 import ('
85 86 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
86 87 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
87 88 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
89 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
88 90 from rhodecode.lib.index.search_utils import get_matching_line_offsets
89 91 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
90 92 from rhodecode.model.changeset_status import ChangesetStatusModel
91 from rhodecode.model.db import Permission, User, Repository
93 from rhodecode.model.db import Permission, User, Repository, UserApiKeys
92 94 from rhodecode.model.repo_group import RepoGroupModel
93 95 from rhodecode.model.settings import IssueTrackerSettingsModel
94 96
@@ -783,13 +785,24 b' flash = Flash()'
783 785 # SCM FILTERS available via h.
784 786 #==============================================================================
785 787 from rhodecode.lib.vcs.utils import author_name, author_email
786 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
788 from rhodecode.lib.utils2 import age, age_from_seconds
787 789 from rhodecode.model.db import User, ChangesetStatus
788 790
789 capitalize = lambda x: x.capitalize()
791
790 792 email = author_email
791 short_id = lambda x: x[:12]
792 hide_credentials = lambda x: ''.join(credentials_filter(x))
793
794
795 def capitalize(raw_text):
796 return raw_text.capitalize()
797
798
799 def short_id(long_id):
800 return long_id[:12]
801
802
803 def hide_credentials(url):
804 from rhodecode.lib.utils2 import credentials_filter
805 return credentials_filter(url)
793 806
794 807
795 808 import pytz
@@ -948,7 +961,7 b' def link_to_user(author, length=0, **kwa'
948 961 if length:
949 962 display_person = shorter(display_person, length)
950 963
951 if user:
964 if user and user.username != user.DEFAULT_USER:
952 965 return link_to(
953 966 escape(display_person),
954 967 route_path('user_profile', username=user.username),
@@ -1341,7 +1354,7 b' class InitialsGravatar(object):'
1341 1354
1342 1355 def generate_svg(self, svg_type=None):
1343 1356 img_data = self.get_img_data(svg_type)
1344 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1357 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1345 1358
1346 1359
1347 1360 def initials_gravatar(email_address, first_name, last_name, size=30):
@@ -400,7 +400,7 b' pre_create_user = ExtensionCallback('
400 400 'admin', 'created_by'))
401 401
402 402
403 log_create_pull_request = ExtensionCallback(
403 create_pull_request = ExtensionCallback(
404 404 hook_name='CREATE_PULL_REQUEST',
405 405 kwargs_keys=(
406 406 'server_url', 'config', 'scm', 'username', 'ip', 'action',
@@ -409,7 +409,7 b' log_create_pull_request = ExtensionCallb'
409 409 'mergeable', 'source', 'target', 'author', 'reviewers'))
410 410
411 411
412 log_merge_pull_request = ExtensionCallback(
412 merge_pull_request = ExtensionCallback(
413 413 hook_name='MERGE_PULL_REQUEST',
414 414 kwargs_keys=(
415 415 'server_url', 'config', 'scm', 'username', 'ip', 'action',
@@ -418,7 +418,7 b' log_merge_pull_request = ExtensionCallba'
418 418 'mergeable', 'source', 'target', 'author', 'reviewers'))
419 419
420 420
421 log_close_pull_request = ExtensionCallback(
421 close_pull_request = ExtensionCallback(
422 422 hook_name='CLOSE_PULL_REQUEST',
423 423 kwargs_keys=(
424 424 'server_url', 'config', 'scm', 'username', 'ip', 'action',
@@ -427,7 +427,7 b' log_close_pull_request = ExtensionCallba'
427 427 'mergeable', 'source', 'target', 'author', 'reviewers'))
428 428
429 429
430 log_review_pull_request = ExtensionCallback(
430 review_pull_request = ExtensionCallback(
431 431 hook_name='REVIEW_PULL_REQUEST',
432 432 kwargs_keys=(
433 433 'server_url', 'config', 'scm', 'username', 'ip', 'action',
@@ -436,7 +436,7 b' log_review_pull_request = ExtensionCallb'
436 436 'mergeable', 'source', 'target', 'author', 'reviewers'))
437 437
438 438
439 log_comment_pull_request = ExtensionCallback(
439 comment_pull_request = ExtensionCallback(
440 440 hook_name='COMMENT_PULL_REQUEST',
441 441 kwargs_keys=(
442 442 'server_url', 'config', 'scm', 'username', 'ip', 'action',
@@ -445,7 +445,16 b' log_comment_pull_request = ExtensionCall'
445 445 'mergeable', 'source', 'target', 'author', 'reviewers'))
446 446
447 447
448 log_update_pull_request = ExtensionCallback(
448 comment_edit_pull_request = ExtensionCallback(
449 hook_name='COMMENT_EDIT_PULL_REQUEST',
450 kwargs_keys=(
451 'server_url', 'config', 'scm', 'username', 'ip', 'action',
452 'repository', 'pull_request_id', 'url', 'title', 'description',
453 'status', 'comment', 'created_on', 'updated_on', 'commit_ids', 'review_status',
454 'mergeable', 'source', 'target', 'author', 'reviewers'))
455
456
457 update_pull_request = ExtensionCallback(
449 458 hook_name='UPDATE_PULL_REQUEST',
450 459 kwargs_keys=(
451 460 'server_url', 'config', 'scm', 'username', 'ip', 'action',
@@ -454,7 +463,7 b' log_update_pull_request = ExtensionCallb'
454 463 'mergeable', 'source', 'target', 'author', 'reviewers'))
455 464
456 465
457 log_create_user = ExtensionCallback(
466 create_user = ExtensionCallback(
458 467 hook_name='CREATE_USER_HOOK',
459 468 kwargs_keys=(
460 469 'username', 'full_name_or_username', 'full_contact', 'user_id',
@@ -465,7 +474,7 b' log_create_user = ExtensionCallback('
465 474 'inherit_default_permissions', 'created_by', 'created_on'))
466 475
467 476
468 log_delete_user = ExtensionCallback(
477 delete_user = ExtensionCallback(
469 478 hook_name='DELETE_USER_HOOK',
470 479 kwargs_keys=(
471 480 'username', 'full_name_or_username', 'full_contact', 'user_id',
@@ -476,7 +485,7 b' log_delete_user = ExtensionCallback('
476 485 'inherit_default_permissions', 'deleted_by'))
477 486
478 487
479 log_create_repository = ExtensionCallback(
488 create_repository = ExtensionCallback(
480 489 hook_name='CREATE_REPO_HOOK',
481 490 kwargs_keys=(
482 491 'repo_name', 'repo_type', 'description', 'private', 'created_on',
@@ -484,7 +493,7 b' log_create_repository = ExtensionCallbac'
484 493 'clone_uri', 'fork_id', 'group_id', 'created_by'))
485 494
486 495
487 log_delete_repository = ExtensionCallback(
496 delete_repository = ExtensionCallback(
488 497 hook_name='DELETE_REPO_HOOK',
489 498 kwargs_keys=(
490 499 'repo_name', 'repo_type', 'description', 'private', 'created_on',
@@ -492,7 +501,7 b' log_delete_repository = ExtensionCallbac'
492 501 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
493 502
494 503
495 log_comment_commit_repository = ExtensionCallback(
504 comment_commit_repository = ExtensionCallback(
496 505 hook_name='COMMENT_COMMIT_REPO_HOOK',
497 506 kwargs_keys=(
498 507 'repo_name', 'repo_type', 'description', 'private', 'created_on',
@@ -500,8 +509,16 b' log_comment_commit_repository = Extensio'
500 509 'clone_uri', 'fork_id', 'group_id',
501 510 'repository', 'created_by', 'comment', 'commit'))
502 511
512 comment_edit_commit_repository = ExtensionCallback(
513 hook_name='COMMENT_EDIT_COMMIT_REPO_HOOK',
514 kwargs_keys=(
515 'repo_name', 'repo_type', 'description', 'private', 'created_on',
516 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
517 'clone_uri', 'fork_id', 'group_id',
518 'repository', 'created_by', 'comment', 'commit'))
503 519
504 log_create_repository_group = ExtensionCallback(
520
521 create_repository_group = ExtensionCallback(
505 522 hook_name='CREATE_REPO_GROUP_HOOK',
506 523 kwargs_keys=(
507 524 'group_name', 'group_parent_id', 'group_description',
@@ -94,7 +94,34 b' def trigger_comment_commit_hooks(usernam'
94 94 extras.commit = commit.serialize()
95 95 extras.comment = comment.get_api_data()
96 96 extras.created_by = username
97 hooks_base.log_comment_commit_repository(**extras)
97 hooks_base.comment_commit_repository(**extras)
98
99
100 def trigger_comment_commit_edit_hooks(username, repo_name, repo_type, repo, data=None):
101 """
102 Triggers when a comment is edited on a commit
103
104 :param username: username who edits the comment
105 :param repo_name: name of target repo
106 :param repo_type: the type of SCM target repo
107 :param repo: the repo object we trigger the event for
108 :param data: extra data for specific events e.g {'comment': comment_obj, 'commit': commit_obj}
109 """
110 if not _supports_repo_type(repo_type):
111 return
112
113 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'comment_commit')
114
115 comment = data['comment']
116 commit = data['commit']
117
118 events.trigger(events.RepoCommitCommentEditEvent(repo, commit, comment))
119 extras.update(repo.get_dict())
120
121 extras.commit = commit.serialize()
122 extras.comment = comment.get_api_data()
123 extras.created_by = username
124 hooks_base.comment_edit_commit_repository(**extras)
98 125
99 126
100 127 def trigger_create_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@@ -113,7 +140,7 b' def trigger_create_pull_request_hook(use'
113 140 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'create_pull_request')
114 141 events.trigger(events.PullRequestCreateEvent(pull_request))
115 142 extras.update(pull_request.get_api_data(with_merge_state=False))
116 hooks_base.log_create_pull_request(**extras)
143 hooks_base.create_pull_request(**extras)
117 144
118 145
119 146 def trigger_merge_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@@ -132,7 +159,7 b' def trigger_merge_pull_request_hook(user'
132 159 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'merge_pull_request')
133 160 events.trigger(events.PullRequestMergeEvent(pull_request))
134 161 extras.update(pull_request.get_api_data())
135 hooks_base.log_merge_pull_request(**extras)
162 hooks_base.merge_pull_request(**extras)
136 163
137 164
138 165 def trigger_close_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@@ -151,7 +178,7 b' def trigger_close_pull_request_hook(user'
151 178 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'close_pull_request')
152 179 events.trigger(events.PullRequestCloseEvent(pull_request))
153 180 extras.update(pull_request.get_api_data())
154 hooks_base.log_close_pull_request(**extras)
181 hooks_base.close_pull_request(**extras)
155 182
156 183
157 184 def trigger_review_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@@ -171,7 +198,7 b' def trigger_review_pull_request_hook(use'
171 198 status = data.get('status')
172 199 events.trigger(events.PullRequestReviewEvent(pull_request, status))
173 200 extras.update(pull_request.get_api_data())
174 hooks_base.log_review_pull_request(**extras)
201 hooks_base.review_pull_request(**extras)
175 202
176 203
177 204 def trigger_comment_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@@ -193,7 +220,29 b' def trigger_comment_pull_request_hook(us'
193 220 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
194 221 extras.update(pull_request.get_api_data())
195 222 extras.comment = comment.get_api_data()
196 hooks_base.log_comment_pull_request(**extras)
223 hooks_base.comment_pull_request(**extras)
224
225
226 def trigger_comment_pull_request_edit_hook(username, repo_name, repo_type, pull_request, data=None):
227 """
228 Triggers when a comment was edited on a pull request
229
230 :param username: username who made the edit
231 :param repo_name: name of target repo
232 :param repo_type: the type of SCM target repo
233 :param pull_request: the pull request that comment was made on
234 :param data: extra data for specific events e.g {'comment': comment_obj}
235 """
236 if not _supports_repo_type(repo_type):
237 return
238
239 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'comment_pull_request')
240
241 comment = data['comment']
242 events.trigger(events.PullRequestCommentEditEvent(pull_request, comment))
243 extras.update(pull_request.get_api_data())
244 extras.comment = comment.get_api_data()
245 hooks_base.comment_edit_pull_request(**extras)
197 246
198 247
199 248 def trigger_update_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@@ -212,4 +261,4 b' def trigger_update_pull_request_hook(use'
212 261 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'update_pull_request')
213 262 events.trigger(events.PullRequestUpdateEvent(pull_request))
214 263 extras.update(pull_request.get_api_data())
215 hooks_base.log_update_pull_request(**extras)
264 hooks_base.update_pull_request(**extras)
@@ -48,6 +48,7 b' from .utils import ('
48 48
49 49
50 50 FILE_TREE_CACHE_VER = 'v4'
51 LICENSE_CACHE_VER = 'v2'
51 52
52 53
53 54 def configure_dogpile_cache(settings):
@@ -159,7 +159,14 b' class FileNamespaceBackend(PickleSeriali'
159 159
160 160 def __init__(self, arguments):
161 161 arguments['lock_factory'] = CustomLockFactory
162 db_file = arguments.get('filename')
163
164 log.debug('initialing %s DB in %s', self.__class__.__name__, db_file)
165 try:
162 166 super(FileNamespaceBackend, self).__init__(arguments)
167 except Exception:
168 log.error('Failed to initialize db at: %s', db_file)
169 raise
163 170
164 171 def __repr__(self):
165 172 return '{} `{}`'.format(self.__class__, self.filename)
@@ -30,6 +30,7 b' import os'
30 30 import re
31 31 import sys
32 32 import shutil
33 import socket
33 34 import tempfile
34 35 import traceback
35 36 import tarfile
@@ -782,3 +783,18 b' def generate_platform_uuid():'
782 783 except Exception as e:
783 784 log.error('Failed to generate host uuid: %s', e)
784 785 return 'UNDEFINED'
786
787
788 def send_test_email(recipients, email_body='TEST EMAIL'):
789 """
790 Simple code for generating test emails.
791 Usage::
792
793 from rhodecode.lib import utils
794 utils.send_test_email()
795 """
796 from rhodecode.lib.celerylib import tasks, run_task
797
798 email_body = email_body_plaintext = email_body
799 subject = 'SUBJECT FROM: {}'.format(socket.gethostname())
800 tasks.send_email(recipients, subject, email_body_plaintext, email_body)
@@ -628,34 +628,42 b' class MercurialRepository(BaseRepository'
628 628 push_branches=push_branches)
629 629
630 630 def _local_merge(self, target_ref, merge_message, user_name, user_email,
631 source_ref, use_rebase=False, dry_run=False):
631 source_ref, use_rebase=False, close_commit_id=None, dry_run=False):
632 632 """
633 633 Merge the given source_revision into the checked out revision.
634 634
635 635 Returns the commit id of the merge and a boolean indicating if the
636 636 commit needs to be pushed.
637 637 """
638 self._update(target_ref.commit_id, clean=True)
638 source_ref_commit_id = source_ref.commit_id
639 target_ref_commit_id = target_ref.commit_id
639 640
640 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
641 # update our workdir to target ref, for proper merge
642 self._update(target_ref_commit_id, clean=True)
643
644 ancestor = self._ancestor(target_ref_commit_id, source_ref_commit_id)
641 645 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
642 646
643 if ancestor == source_ref.commit_id:
644 # Nothing to do, the changes were already integrated
645 return target_ref.commit_id, False
647 if close_commit_id:
648 # NOTE(marcink): if we get the close commit, this is our new source
649 # which will include the close commit itself.
650 source_ref_commit_id = close_commit_id
646 651
647 elif ancestor == target_ref.commit_id and is_the_same_branch:
652 if ancestor == source_ref_commit_id:
653 # Nothing to do, the changes were already integrated
654 return target_ref_commit_id, False
655
656 elif ancestor == target_ref_commit_id and is_the_same_branch:
648 657 # In this case we should force a commit message
649 return source_ref.commit_id, True
658 return source_ref_commit_id, True
650 659
651 660 unresolved = None
652 661 if use_rebase:
653 662 try:
654 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
655 target_ref.commit_id)
663 bookmark_name = 'rcbook%s%s' % (source_ref_commit_id, target_ref_commit_id)
656 664 self.bookmark(bookmark_name, revision=source_ref.commit_id)
657 665 self._remote.rebase(
658 source=source_ref.commit_id, dest=target_ref.commit_id)
666 source=source_ref_commit_id, dest=target_ref_commit_id)
659 667 self._remote.invalidate_vcs_cache()
660 668 self._update(bookmark_name, clean=True)
661 669 return self._identify(), True
@@ -678,7 +686,7 b' class MercurialRepository(BaseRepository'
678 686 raise
679 687 else:
680 688 try:
681 self._remote.merge(source_ref.commit_id)
689 self._remote.merge(source_ref_commit_id)
682 690 self._remote.invalidate_vcs_cache()
683 691 self._remote.commit(
684 692 message=safe_str(merge_message),
@@ -820,10 +828,12 b' class MercurialRepository(BaseRepository'
820 828
821 829 needs_push = False
822 830 if merge_possible:
831
823 832 try:
824 833 merge_commit_id, needs_push = shadow_repo._local_merge(
825 834 target_ref, merge_message, merger_name, merger_email,
826 source_ref, use_rebase=use_rebase, dry_run=dry_run)
835 source_ref, use_rebase=use_rebase,
836 close_commit_id=close_commit_id, dry_run=dry_run)
827 837 merge_possible = True
828 838
829 839 # read the state of the close action, if it
@@ -41,7 +41,7 b' BACKENDS = {'
41 41
42 42
43 43 ARCHIVE_SPECS = [
44 ('tbz2', 'application/x-bzip2', 'tbz2'),
44 ('tbz2', 'application/x-bzip2', '.tbz2'),
45 45 ('tbz2', 'application/x-bzip2', '.tar.bz2'),
46 46
47 47 ('tgz', 'application/x-gzip', '.tgz'),
@@ -21,6 +21,7 b''
21 21 """
22 22 comments model for RhodeCode
23 23 """
24 import datetime
24 25
25 26 import logging
26 27 import traceback
@@ -32,10 +33,17 b' from sqlalchemy.sql.functions import coa'
32 33
33 34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 35 from rhodecode.lib import audit_logger
35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
36 38 from rhodecode.model import BaseModel
37 39 from rhodecode.model.db import (
38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
40 ChangesetComment,
41 User,
42 Notification,
43 PullRequest,
44 AttributeDict,
45 ChangesetCommentHistory,
46 )
39 47 from rhodecode.model.notification import NotificationModel
40 48 from rhodecode.model.meta import Session
41 49 from rhodecode.model.settings import VcsSettingsModel
@@ -362,13 +370,18 b' class CommentsModel(BaseModel):'
362 370 repo.repo_name,
363 371 h.route_url('repo_summary', repo_name=repo.repo_name))
364 372
373 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
374 commit_id=commit_id)
375
365 376 # commit specifics
366 377 kwargs.update({
367 378 'commit': commit_obj,
368 379 'commit_message': commit_obj.message,
369 380 'commit_target_repo_url': target_repo_url,
370 381 'commit_comment_url': commit_comment_url,
371 'commit_comment_reply_url': commit_comment_reply_url
382 'commit_comment_reply_url': commit_comment_reply_url,
383 'commit_url': commit_url,
384 'thread_ids': [commit_url, commit_comment_url],
372 385 })
373 386
374 387 elif pull_request_obj:
@@ -413,15 +426,14 b' class CommentsModel(BaseModel):'
413 426 'pr_comment_url': pr_comment_url,
414 427 'pr_comment_reply_url': pr_comment_reply_url,
415 428 'pr_closing': closing_pr,
429 'thread_ids': [pr_url, pr_comment_url],
416 430 })
417 431
418 432 recipients += [self._get_user(u) for u in (extra_recipients or [])]
419 433
420 434 if send_email:
421 435 # pre-generate the subject for notification itself
422 (subject,
423 _h, _e, # we don't care about those
424 body_plaintext) = EmailNotificationModel().render_email(
436 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
425 437 notification_type, **kwargs)
426 438
427 439 mention_recipients = set(
@@ -479,6 +491,60 b' class CommentsModel(BaseModel):'
479 491
480 492 return comment
481 493
494 def edit(self, comment_id, text, auth_user, version):
495 """
496 Change existing comment for commit or pull request.
497
498 :param comment_id:
499 :param text:
500 :param auth_user: current authenticated user calling this method
501 :param version: last comment version
502 """
503 if not text:
504 log.warning('Missing text for comment, skipping...')
505 return
506
507 comment = ChangesetComment.get(comment_id)
508 old_comment_text = comment.text
509 comment.text = text
510 comment.modified_at = datetime.datetime.now()
511 version = safe_int(version)
512
513 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
514 # would return 3 here
515 comment_version = ChangesetCommentHistory.get_version(comment_id)
516
517 if isinstance(version, (int, long)) and (comment_version - version) != 1:
518 log.warning(
519 'Version mismatch comment_version {} submitted {}, skipping'.format(
520 comment_version-1, # -1 since note above
521 version
522 )
523 )
524 raise CommentVersionMismatch()
525
526 comment_history = ChangesetCommentHistory()
527 comment_history.comment_id = comment_id
528 comment_history.version = comment_version
529 comment_history.created_by_user_id = auth_user.user_id
530 comment_history.text = old_comment_text
531 # TODO add email notification
532 Session().add(comment_history)
533 Session().add(comment)
534 Session().flush()
535
536 if comment.pull_request:
537 action = 'repo.pull_request.comment.edit'
538 else:
539 action = 'repo.commit.comment.edit'
540
541 comment_data = comment.get_api_data()
542 comment_data['old_comment_text'] = old_comment_text
543 self._log_audit_action(
544 action, {'data': comment_data}, auth_user, comment)
545
546 return comment_history
547
482 548 def delete(self, comment, auth_user):
483 549 """
484 550 Deletes given comment
@@ -712,6 +778,7 b' class CommentsModel(BaseModel):'
712 778 .filter(ChangesetComment.line_no == None)\
713 779 .filter(ChangesetComment.f_path == None)\
714 780 .filter(ChangesetComment.pull_request == pull_request)
781
715 782 return comments
716 783
717 784 @staticmethod
@@ -726,8 +793,7 b' class CommentsModel(BaseModel):'
726 793 if action == 'create':
727 794 trigger_hook = hooks_utils.trigger_comment_commit_hooks
728 795 elif action == 'edit':
729 # TODO(dan): when this is supported we trigger edit hook too
730 return
796 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
731 797 else:
732 798 return
733 799
@@ -103,7 +103,12 b' def display_user_sort(obj):'
103 103 if obj.username == User.DEFAULT_USER:
104 104 return '#####'
105 105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 return prefix + obj.username
106 extra_sort_num = '1' # default
107
108 # NOTE(dan): inactive duplicates goes last
109 if getattr(obj, 'duplicate_perm', None):
110 extra_sort_num = '9'
111 return prefix + extra_sort_num + obj.username
107 112
108 113
109 114 def display_user_group_sort(obj):
@@ -1128,14 +1133,16 b' class UserApiKeys(Base, BaseModel):'
1128 1133
1129 1134 # ApiKey role
1130 1135 ROLE_ALL = 'token_role_all'
1131 ROLE_HTTP = 'token_role_http'
1132 1136 ROLE_VCS = 'token_role_vcs'
1133 1137 ROLE_API = 'token_role_api'
1138 ROLE_HTTP = 'token_role_http'
1134 1139 ROLE_FEED = 'token_role_feed'
1135 1140 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1141 # The last one is ignored in the list as we only
1142 # use it for one action, and cannot be created by users
1136 1143 ROLE_PASSWORD_RESET = 'token_password_reset'
1137 1144
1138 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1145 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1139 1146
1140 1147 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1141 1148 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
@@ -1200,6 +1207,22 b' class UserApiKeys(Base, BaseModel):'
1200 1207 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1201 1208 }.get(role, role)
1202 1209
1210 @classmethod
1211 def _get_role_description(cls, role):
1212 return {
1213 cls.ROLE_ALL: _('Token for all actions.'),
1214 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1215 'login using `api_access_controllers_whitelist` functionality.'),
1216 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1217 'Requires auth_token authentication plugin to be active. <br/>'
1218 'Such Token should be used then instead of a password to '
1219 'interact with a repository, and additionally can be '
1220 'limited to single repository using repo scope.'),
1221 cls.ROLE_API: _('Token limited to api calls.'),
1222 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1223 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1224 }.get(role, role)
1225
1203 1226 @property
1204 1227 def role_humanized(self):
1205 1228 return self._get_role_name(self.role)
@@ -3755,6 +3778,7 b' class ChangesetComment(Base, BaseModel):'
3755 3778 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3756 3779 pull_request = relationship('PullRequest', lazy='joined')
3757 3780 pull_request_version = relationship('PullRequestVersion')
3781 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
3758 3782
3759 3783 @classmethod
3760 3784 def get_users(cls, revision=None, pull_request_id=None):
@@ -3805,6 +3829,11 b' class ChangesetComment(Base, BaseModel):'
3805 3829 return self.pull_request_version_id < version
3806 3830
3807 3831 @property
3832 def commit_id(self):
3833 """New style naming to stop using .revision"""
3834 return self.revision
3835
3836 @property
3808 3837 def resolved(self):
3809 3838 return self.resolved_by[0] if self.resolved_by else None
3810 3839
@@ -3816,6 +3845,13 b' class ChangesetComment(Base, BaseModel):'
3816 3845 def is_inline(self):
3817 3846 return self.line_no and self.f_path
3818 3847
3848 @property
3849 def last_version(self):
3850 version = 0
3851 if self.history:
3852 version = self.history[-1].version
3853 return version
3854
3819 3855 def get_index_version(self, versions):
3820 3856 return self.get_index_from_version(
3821 3857 self.pull_request_version_id, versions)
@@ -3828,6 +3864,7 b' class ChangesetComment(Base, BaseModel):'
3828 3864
3829 3865 def get_api_data(self):
3830 3866 comment = self
3867
3831 3868 data = {
3832 3869 'comment_id': comment.comment_id,
3833 3870 'comment_type': comment.comment_type,
@@ -3840,6 +3877,7 b' class ChangesetComment(Base, BaseModel):'
3840 3877 'comment_resolved_by': self.resolved,
3841 3878 'comment_commit_id': comment.revision,
3842 3879 'comment_pull_request_id': comment.pull_request_id,
3880 'comment_last_version': self.last_version
3843 3881 }
3844 3882 return data
3845 3883
@@ -3849,6 +3887,36 b' class ChangesetComment(Base, BaseModel):'
3849 3887 return data
3850 3888
3851 3889
3890 class ChangesetCommentHistory(Base, BaseModel):
3891 __tablename__ = 'changeset_comments_history'
3892 __table_args__ = (
3893 Index('cch_comment_id_idx', 'comment_id'),
3894 base_table_args,
3895 )
3896
3897 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3898 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3899 version = Column("version", Integer(), nullable=False, default=0)
3900 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3901 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3902 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3903 deleted = Column('deleted', Boolean(), default=False)
3904
3905 author = relationship('User', lazy='joined')
3906 comment = relationship('ChangesetComment', cascade="all, delete")
3907
3908 @classmethod
3909 def get_version(cls, comment_id):
3910 q = Session().query(ChangesetCommentHistory).filter(
3911 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3912 if q.count() == 0:
3913 return 1
3914 elif q.count() >= q[0].version:
3915 return q.count() + 1
3916 else:
3917 return q[0].version + 1
3918
3919
3852 3920 class ChangesetStatus(Base, BaseModel):
3853 3921 __tablename__ = 'changeset_statuses'
3854 3922 __table_args__ = (
@@ -131,15 +131,17 b' class NotificationModel(BaseModel):'
131 131 # inject current recipient
132 132 email_kwargs['recipient'] = recipient
133 133 email_kwargs['mention'] = recipient in mention_recipients
134 (subject, headers, email_body,
135 email_body_plaintext) = EmailNotificationModel().render_email(
134 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
136 135 notification_type, **email_kwargs)
137 136
138 log.debug(
139 'Creating notification email task for user:`%s`', recipient)
137 extra_headers = None
138 if 'thread_ids' in email_kwargs:
139 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
140
141 log.debug('Creating notification email task for user:`%s`', recipient)
140 142 task = run_task(
141 143 tasks.send_email, recipient.email, subject,
142 email_body_plaintext, email_body)
144 email_body_plaintext, email_body, extra_headers=extra_headers)
143 145 log.debug('Created email task: %s', task)
144 146
145 147 return notification
@@ -293,6 +295,27 b' class NotificationModel(BaseModel):'
293 295 }
294 296
295 297
298 # Templates for Titles, that could be overwritten by rcextensions
299 # Title of email for pull-request update
300 EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
301 # Title of email for request for pull request review
302 EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
303
304 # Title of email for general comment on pull request
305 EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
306 # Title of email for general comment which includes status change on pull request
307 EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
308 # Title of email for inline comment on a file in pull request
309 EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
310
311 # Title of email for general comment on commit
312 EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
313 # Title of email for general comment which includes status change on commit
314 EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
315 # Title of email for inline comment on a file in commit
316 EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
317
318
296 319 class EmailNotificationModel(BaseModel):
297 320 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
298 321 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
@@ -333,7 +356,7 b' class EmailNotificationModel(BaseModel):'
333 356 }
334 357
335 358 premailer_instance = premailer.Premailer(
336 cssutils_logging_level=logging.WARNING,
359 cssutils_logging_level=logging.ERROR,
337 360 cssutils_logging_handler=logging.getLogger().handlers[0]
338 361 if logging.getLogger().handlers else None,
339 362 )
@@ -342,8 +365,7 b' class EmailNotificationModel(BaseModel):'
342 365 """
343 366 Example usage::
344 367
345 (subject, headers, email_body,
346 email_body_plaintext) = EmailNotificationModel().render_email(
368 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
347 369 EmailNotificationModel.TYPE_TEST, **email_kwargs)
348 370
349 371 """
@@ -387,12 +409,6 b' class EmailNotificationModel(BaseModel):'
387 409 subject = email_template.render('subject', **_kwargs)
388 410
389 411 try:
390 headers = email_template.render('headers', **_kwargs)
391 except AttributeError:
392 # it's not defined in template, ok we can skip it
393 headers = ''
394
395 try:
396 412 body_plaintext = email_template.render('body_plaintext', **_kwargs)
397 413 except AttributeError:
398 414 # it's not defined in template, ok we can skip it
@@ -408,4 +424,4 b' class EmailNotificationModel(BaseModel):'
408 424 log.exception('Failed to parse body with premailer')
409 425 pass
410 426
411 return subject, headers, body, body_plaintext
427 return subject, body, body_plaintext
@@ -577,7 +577,8 b' class PermissionModel(BaseModel):'
577 577 user_group_write_permissions[p.users_group_id] = p
578 578 return user_group_write_permissions
579 579
580 def trigger_permission_flush(self, affected_user_ids):
580 def trigger_permission_flush(self, affected_user_ids=None):
581 affected_user_ids or User.get_all_user_ids()
581 582 events.trigger(events.UserPermissionsChange(affected_user_ids))
582 583
583 584 def flush_user_permission_caches(self, changes, affected_user_ids=None):
@@ -703,6 +703,8 b' class PullRequestModel(BaseModel):'
703 703 trigger_hook = hooks_utils.trigger_update_pull_request_hook
704 704 elif action == 'comment':
705 705 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
706 elif action == 'comment_edit':
707 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
706 708 else:
707 709 return
708 710
@@ -1342,12 +1344,11 b' class PullRequestModel(BaseModel):'
1342 1344 'pull_request_source_repo_url': pr_source_repo_url,
1343 1345
1344 1346 'pull_request_url': pr_url,
1347 'thread_ids': [pr_url],
1345 1348 }
1346 1349
1347 1350 # pre-generate the subject for notification itself
1348 (subject,
1349 _h, _e, # we don't care about those
1350 body_plaintext) = EmailNotificationModel().render_email(
1351 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1351 1352 notification_type, **kwargs)
1352 1353
1353 1354 # create notification objects, and emails
@@ -1412,11 +1413,10 b' class PullRequestModel(BaseModel):'
1412 1413 'added_files': file_changes.added,
1413 1414 'modified_files': file_changes.modified,
1414 1415 'removed_files': file_changes.removed,
1416 'thread_ids': [pr_url],
1415 1417 }
1416 1418
1417 (subject,
1418 _h, _e, # we don't care about those
1419 body_plaintext) = EmailNotificationModel().render_email(
1419 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1420 1420 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1421 1421
1422 1422 # create notification objects, and emails
@@ -2053,9 +2053,9 b' class MergeCheck(object):'
2053 2053 repo_type = pull_request.target_repo.repo_type
2054 2054 close_msg = ''
2055 2055 if repo_type == 'hg':
2056 close_msg = _('Source branch will be closed after merge.')
2056 close_msg = _('Source branch will be closed before the merge.')
2057 2057 elif repo_type == 'git':
2058 close_msg = _('Source branch will be deleted after merge.')
2058 close_msg = _('Source branch will be deleted after the merge.')
2059 2059
2060 2060 merge_details['close_branch'] = dict(
2061 2061 details={},
@@ -33,7 +33,7 b' from rhodecode import events'
33 33 from rhodecode.lib.auth import HasUserGroupPermissionAny
34 34 from rhodecode.lib.caching_query import FromCache
35 35 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
36 from rhodecode.lib.hooks_base import log_delete_repository
36 from rhodecode.lib import hooks_base
37 37 from rhodecode.lib.user_log_filter import user_log_filter
38 38 from rhodecode.lib.utils import make_db_config
39 39 from rhodecode.lib.utils2 import (
@@ -767,7 +767,7 b' class RepoModel(BaseModel):'
767 767 'deleted_by': cur_user,
768 768 'deleted_on': time.time(),
769 769 })
770 log_delete_repository(**old_repo_dict)
770 hooks_base.delete_repository(**old_repo_dict)
771 771 events.trigger(events.RepoDeleteEvent(repo))
772 772 except Exception:
773 773 log.error(traceback.format_exc())
@@ -308,13 +308,13 b' class RepoGroupModel(BaseModel):'
308 308 self._create_group(new_repo_group.group_name)
309 309
310 310 # trigger the post hook
311 from rhodecode.lib.hooks_base import log_create_repository_group
311 from rhodecode.lib import hooks_base
312 312 repo_group = RepoGroup.get_by_group_name(group_name)
313 313
314 314 # update repo group commit caches initially
315 315 repo_group.update_commit_cache()
316 316
317 log_create_repository_group(
317 hooks_base.create_repository_group(
318 318 created_by=user.username, **repo_group.get_dict())
319 319
320 320 # Trigger create event.
@@ -262,8 +262,7 b' class UserModel(BaseModel):'
262 262
263 263 from rhodecode.lib.auth import (
264 264 get_crypt_password, check_password)
265 from rhodecode.lib.hooks_base import (
266 log_create_user, check_allowed_create_user)
265 from rhodecode.lib import hooks_base
267 266
268 267 def _password_change(new_user, password):
269 268 old_password = new_user.password or ''
@@ -327,7 +326,7 b' class UserModel(BaseModel):'
327 326 if new_active_user and strict_creation_check:
328 327 # raises UserCreationError if it's not allowed for any reason to
329 328 # create new active user, this also executes pre-create hooks
330 check_allowed_create_user(user_data, cur_user, strict_check=True)
329 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
331 330 events.trigger(events.UserPreCreate(user_data))
332 331 new_user = User()
333 332 edit = False
@@ -390,7 +389,7 b' class UserModel(BaseModel):'
390 389 kwargs = new_user.get_dict()
391 390 # backward compat, require api_keys present
392 391 kwargs['api_keys'] = kwargs['auth_tokens']
393 log_create_user(created_by=cur_user, **kwargs)
392 hooks_base.create_user(created_by=cur_user, **kwargs)
394 393 events.trigger(events.UserPostCreate(user_data))
395 394 return new_user
396 395 except (DatabaseError,):
@@ -423,9 +422,7 b' class UserModel(BaseModel):'
423 422 }
424 423 notification_type = EmailNotificationModel.TYPE_REGISTRATION
425 424 # pre-generate the subject for notification itself
426 (subject,
427 _h, _e, # we don't care about those
428 body_plaintext) = EmailNotificationModel().render_email(
425 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
429 426 notification_type, **kwargs)
430 427
431 428 # create notification objects, and emails
@@ -569,7 +566,7 b' class UserModel(BaseModel):'
569 566 def delete(self, user, cur_user=None, handle_repos=None,
570 567 handle_repo_groups=None, handle_user_groups=None,
571 568 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
572 from rhodecode.lib.hooks_base import log_delete_user
569 from rhodecode.lib import hooks_base
573 570
574 571 if not cur_user:
575 572 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
@@ -638,7 +635,7 b' class UserModel(BaseModel):'
638 635 self.sa.expire(user)
639 636 self.sa.delete(user)
640 637
641 log_delete_user(deleted_by=cur_user, **user_data)
638 hooks_base.delete_user(deleted_by=cur_user, **user_data)
642 639 except Exception:
643 640 log.error(traceback.format_exc())
644 641 raise
@@ -660,8 +657,7 b' class UserModel(BaseModel):'
660 657 'first_admin_email': User.get_first_super_admin().email
661 658 }
662 659
663 (subject, headers, email_body,
664 email_body_plaintext) = EmailNotificationModel().render_email(
660 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
665 661 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
666 662
667 663 recipients = [user_email]
@@ -719,8 +715,7 b' class UserModel(BaseModel):'
719 715 'first_admin_email': User.get_first_super_admin().email
720 716 }
721 717
722 (subject, headers, email_body,
723 email_body_plaintext) = EmailNotificationModel().render_email(
718 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
724 719 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
725 720 **email_kwargs)
726 721
@@ -53,7 +53,8 b' def deferred_can_write_to_group_validato'
53 53 # permissions denied we expose as not existing, to prevent
54 54 # resource discovery
55 55 'permission_denied_parent_group':
56 _(u"Parent repository group `{}` does not exist"),
56 _(u"You do not have the permissions to store "
57 u"repository groups inside repository group `{}`"),
57 58 'permission_denied_root':
58 59 _(u"You do not have the permission to store "
59 60 u"repository groups in the root location.")
@@ -100,9 +101,15 b' def deferred_can_write_to_group_validato'
100 101 # we want to allow this...
101 102 forbidden = not (group_admin or (group_write and create_on_write and 0))
102 103
104 old_name = old_values.get('group_name')
105 if old_name and old_name == old_values.get('submitted_repo_group_name'):
106 # we're editing a repository group, we didn't change the name
107 # we skip the check for write into parent group now
108 # this allows changing settings for this repo group
109 return
110
103 111 if parent_group and forbidden:
104 msg = messages['permission_denied_parent_group'].format(
105 parent_group_name)
112 msg = messages['permission_denied_parent_group'].format(parent_group_name)
106 113 raise colander.Invalid(node, msg)
107 114
108 115 return can_write_group_validator
@@ -248,6 +255,9 b' class RepoGroupSchema(colander.Schema):'
248 255 validated_name = appstruct['repo_group_name']
249 256
250 257 # second pass to validate permissions to repo_group
258 if 'old_values' in self.bindings:
259 # save current repo name for name change checks
260 self.bindings['old_values']['submitted_repo_group_name'] = validated_name
251 261 second = RepoGroupAccessSchema().bind(**self.bindings)
252 262 appstruct_second = second.deserialize({'repo_group': validated_name})
253 263 # save result
@@ -286,6 +296,9 b' class RepoGroupSettingsSchema(RepoGroupS'
286 296 validated_name = separator.join([group.group_name, validated_name])
287 297
288 298 # second pass to validate permissions to repo_group
299 if 'old_values' in self.bindings:
300 # save current repo name for name change checks
301 self.bindings['old_values']['submitted_repo_group_name'] = validated_name
289 302 second = RepoGroupAccessSchema().bind(**self.bindings)
290 303 appstruct_second = second.deserialize({'repo_group': validated_name})
291 304 # save result
@@ -141,17 +141,23 b' def deferred_can_write_to_group_validato'
141 141
142 142 is_root_location = value is types.RootLocation
143 143 # NOT initialized validators, we must call them
144 can_create_repos_at_root = HasPermissionAny(
145 'hg.admin', 'hg.create.repository')
144 can_create_repos_at_root = HasPermissionAny('hg.admin', 'hg.create.repository')
146 145
147 146 # if values is root location, we simply need to check if we can write
148 147 # to root location !
149 148 if is_root_location:
149
150 150 if can_create_repos_at_root(user=request_user):
151 151 # we can create repo group inside tool-level. No more checks
152 152 # are required
153 153 return
154 154 else:
155 old_name = old_values.get('repo_name')
156 if old_name and old_name == old_values.get('submitted_repo_name'):
157 # since we didn't change the name, we can skip validation and
158 # allow current users without store-in-root permissions to update
159 return
160
155 161 # "fake" node name as repo_name, otherwise we oddly report
156 162 # the error as if it was coming form repo_group
157 163 # however repo_group is empty when using root location.
@@ -372,6 +378,9 b' class RepoSchema(colander.MappingSchema)'
372 378 validated_name = appstruct['repo_name']
373 379
374 380 # second pass to validate permissions to repo_group
381 if 'old_values' in self.bindings:
382 # save current repo name for name change checks
383 self.bindings['old_values']['submitted_repo_name'] = validated_name
375 384 second = RepoGroupAccessSchema().bind(**self.bindings)
376 385 appstruct_second = second.deserialize({'repo_group': validated_name})
377 386 # save result
@@ -429,6 +438,9 b' class RepoSettingsSchema(RepoSchema):'
429 438 validated_name = separator.join([group.group_name, validated_name])
430 439
431 440 # second pass to validate permissions to repo_group
441 if 'old_values' in self.bindings:
442 # save current repo name for name change checks
443 self.bindings['old_values']['submitted_repo_name'] = validated_name
432 444 second = RepoGroupAccessSchema().bind(**self.bindings)
433 445 appstruct_second = second.deserialize({'repo_group': validated_name})
434 446 # save result
@@ -259,21 +259,34 b' input[type="button"] {'
259 259 &:not(.open) .btn-action-switcher-container {
260 260 display: none;
261 261 }
262
263 .btn-more-option {
264 margin-left: -1px;
265 padding-left: 2px;
266 padding-right: 2px;
267 border-left: 1px solid @grey3;
268 }
262 269 }
263 270
264 271
265 272 .btn-action-switcher-container{
266 273 position: absolute;
267 top: 30px;
268 left: -82px;
274 top: 100%;
275
276 &.left-align {
277 left: 0;
278 }
279 &.right-align {
280 right: 0;
281 }
282
269 283 }
270 284
271 285 .btn-action-switcher {
272 286 display: block;
273 287 position: relative;
274 288 z-index: 300;
275 min-width: 240px;
276 max-width: 500px;
289 max-width: 600px;
277 290 margin-top: 4px;
278 291 margin-bottom: 24px;
279 292 font-size: 14px;
@@ -283,6 +296,7 b' input[type="button"] {'
283 296 border: 1px solid @grey4;
284 297 border-radius: 3px;
285 298 box-shadow: @dropdown-shadow;
299 overflow: auto;
286 300
287 301 li {
288 302 display: block;
@@ -998,6 +998,21 b' input.filediff-collapse-state {'
998 998
999 999 /**** END COMMENTS ****/
1000 1000
1001
1002 .nav-chunk {
1003 position: absolute;
1004 right: 20px;
1005 margin-top: -17px;
1006 }
1007
1008 .nav-chunk.selected {
1009 visibility: visible !important;
1010 }
1011
1012 #diff_nav {
1013 color: @grey3;
1014 }
1015
1001 1016 }
1002 1017
1003 1018
@@ -1063,6 +1078,10 b' input.filediff-collapse-state {'
1063 1078 background: @color5;
1064 1079 color: white;
1065 1080 }
1081 &[op="comments"] { /* comments on file */
1082 background: @grey4;
1083 color: white;
1084 }
1066 1085 }
1067 1086 }
1068 1087
@@ -65,7 +65,7 b' tr.inline-comments div {'
65 65 float: left;
66 66
67 67 padding: 0.4em 0.4em;
68 margin: 3px 5px 0px -10px;
68 margin: 2px 4px 0px 0px;
69 69 display: inline-block;
70 70 min-height: 0;
71 71
@@ -76,12 +76,13 b' tr.inline-comments div {'
76 76 font-family: @text-italic;
77 77 font-style: italic;
78 78 background: #fff none;
79 color: @grey4;
79 color: @grey3;
80 80 border: 1px solid @grey4;
81 81 white-space: nowrap;
82 82
83 83 text-transform: uppercase;
84 min-width: 40px;
84 min-width: 50px;
85 border-radius: 4px;
85 86
86 87 &.todo {
87 88 color: @color5;
@@ -253,12 +254,10 b' tr.inline-comments div {'
253 254 }
254 255
255 256 .pr-version {
256 float: left;
257 margin: 0px 4px;
257 display: inline-block;
258 258 }
259 259 .pr-version-inline {
260 float: left;
261 margin: 0px 4px;
260 display: inline-block;
262 261 }
263 262 .pr-version-num {
264 263 font-size: 10px;
@@ -447,6 +446,13 b' form.comment-form {'
447 446 }
448 447 }
449 448
449 .comment-version-select {
450 margin: 0px;
451 border-radius: inherit;
452 border-color: @grey6;
453 height: 20px;
454 }
455
450 456 .comment-type {
451 457 margin: 0px;
452 458 border-radius: inherit;
@@ -97,6 +97,11 b' input + .action-link, .action-link.first'
97 97 border-left: none;
98 98 }
99 99
100 .link-disabled {
101 color: @grey4;
102 cursor: default;
103 }
104
100 105 .action-link.last{
101 106 margin-right: @padding;
102 107 padding-right: @padding;
@@ -148,6 +148,38 b' select.select2{height:28px;visibility:hi'
148 148 margin: 0;
149 149 }
150 150
151
152 .drop-menu-comment-history {
153 .drop-menu-core;
154 border: none;
155 padding: 0 6px 0 0;
156 width: auto;
157 min-width: 0;
158 margin: 0;
159 position: relative;
160 display: inline-block;
161 line-height: 1em;
162 z-index: 2;
163 cursor: pointer;
164
165 a {
166 display:block;
167 padding: 0;
168 position: relative;
169
170 &:after {
171 position: absolute;
172 content: "\00A0\25BE";
173 right: -0.80em;
174 line-height: 1em;
175 top: -0.20em;
176 width: 1em;
177 font-size: 16px;
178 }
179 }
180
181 }
182
151 183 .field-sm .drop-menu {
152 184 padding: 1px 0 0 0;
153 185 a {
@@ -33,6 +33,12 b' table.dataTable {'
33 33 .rc-user {
34 34 white-space: nowrap;
35 35 }
36 .user-perm-duplicate {
37 color: @grey4;
38 a {
39 color: @grey4;
40 }
41 }
36 42 }
37 43
38 44 .td-email {
@@ -37,6 +37,10 b''
37 37 &:hover {
38 38 border-color: @grey4;
39 39 }
40
41 &.authortag {
42 padding: 2px;
43 }
40 44 }
41 45
42 46 .tag0 { .border ( @border-thickness-tags, @grey4 ); color:@grey4; }
@@ -185,8 +185,10 b' function registerRCRoutes() {'
185 185 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
186 186 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
187 187 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
188 pyroutes.register('repo_commit_comment_history_view', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_history_id)s/history_view', ['repo_name', 'commit_id', 'comment_history_id']);
188 189 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
189 190 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
191 pyroutes.register('repo_commit_comment_edit', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/edit', ['repo_name', 'commit_id', 'comment_id']);
190 192 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
191 193 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
192 194 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
@@ -242,6 +244,7 b' function registerRCRoutes() {'
242 244 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
243 245 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
244 246 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
247 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']);
245 248 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']);
246 249 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
247 250 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
@@ -9,6 +9,7 b''
9 9 margin: 0;
10 10 float: right;
11 11 cursor: pointer;
12 padding: 8px 0 8px 8px;
12 13 }
13 14
14 15 .toast-message-holder{
@@ -80,9 +80,10 b' var _submitAjaxPOST = function(url, post'
80 80 })(function() {
81 81 "use strict";
82 82
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84
84 85 if (!(this instanceof CommentForm)) {
85 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
86 87 }
87 88
88 89 // bind the element instance to our Form
@@ -126,10 +127,20 b' var _submitAjaxPOST = function(url, post'
126 127 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127 128 this.submitButtonText = this.submitButton.val();
128 129
130
129 131 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
130 132 {'repo_name': templateContext.repo_name,
131 133 'commit_id': templateContext.commit_data.commit_id});
132 134
135 if (edit){
136 this.submitButtonText = _gettext('Updated Comment');
137 $(this.commentType).prop('disabled', true);
138 $(this.commentType).addClass('disabled');
139 var editInfo =
140 '';
141 $(editInfo).insertBefore($(this.editButton).parent());
142 }
143
133 144 if (resolvesCommentId){
134 145 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
135 146 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
@@ -153,17 +164,27 b' var _submitAjaxPOST = function(url, post'
153 164 // based on commitId, or pullRequestId decide where do we submit
154 165 // out data
155 166 if (this.commitId){
156 this.submitUrl = pyroutes.url('repo_commit_comment_create',
167 var pyurl = 'repo_commit_comment_create';
168 if(edit){
169 pyurl = 'repo_commit_comment_edit';
170 }
171 this.submitUrl = pyroutes.url(pyurl,
157 172 {'repo_name': templateContext.repo_name,
158 'commit_id': this.commitId});
173 'commit_id': this.commitId,
174 'comment_id': comment_id});
159 175 this.selfUrl = pyroutes.url('repo_commit',
160 176 {'repo_name': templateContext.repo_name,
161 177 'commit_id': this.commitId});
162 178
163 179 } else if (this.pullRequestId) {
164 this.submitUrl = pyroutes.url('pullrequest_comment_create',
180 var pyurl = 'pullrequest_comment_create';
181 if(edit){
182 pyurl = 'pullrequest_comment_edit';
183 }
184 this.submitUrl = pyroutes.url(pyurl,
165 185 {'repo_name': templateContext.repo_name,
166 'pull_request_id': this.pullRequestId});
186 'pull_request_id': this.pullRequestId,
187 'comment_id': comment_id});
167 188 this.selfUrl = pyroutes.url('pullrequest_show',
168 189 {'repo_name': templateContext.repo_name,
169 190 'pull_request_id': this.pullRequestId});
@@ -277,7 +298,7 b' var _submitAjaxPOST = function(url, post'
277 298 this.globalSubmitSuccessCallback = function(){
278 299 // default behaviour is to call GLOBAL hook, if it's registered.
279 300 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
280 commentFormGlobalSubmitSuccessCallback()
301 commentFormGlobalSubmitSuccessCallback();
281 302 }
282 303 };
283 304
@@ -475,6 +496,43 b' var _submitAjaxPOST = function(url, post'
475 496 return CommentForm;
476 497 });
477 498
499 /* selector for comment versions */
500 var initVersionSelector = function(selector, initialData) {
501
502 var formatResult = function(result, container, query, escapeMarkup) {
503
504 return renderTemplate('commentVersion', {
505 show_disabled: true,
506 version: result.comment_version,
507 user_name: result.comment_author_username,
508 gravatar_url: result.comment_author_gravatar,
509 size: 16,
510 timeago_component: result.comment_created_on,
511 })
512 };
513
514 $(selector).select2({
515 placeholder: "Edited",
516 containerCssClass: "drop-menu-comment-history",
517 dropdownCssClass: "drop-menu-dropdown",
518 dropdownAutoWidth: true,
519 minimumResultsForSearch: -1,
520 data: initialData,
521 formatResult: formatResult,
522 });
523
524 $(selector).on('select2-selecting', function (e) {
525 // hide the mast as we later do preventDefault()
526 $("#select2-drop-mask").click();
527 e.preventDefault();
528 e.choice.action();
529 });
530
531 $(selector).on("select2-open", function() {
532 timeagoActivate();
533 });
534 };
535
478 536 /* comments controller */
479 537 var CommentsController = function() {
480 538 var mainComment = '#text';
@@ -482,11 +540,53 b' var CommentsController = function() {'
482 540
483 541 this.cancelComment = function(node) {
484 542 var $node = $(node);
485 var $td = $node.closest('td');
543 var edit = $(this).attr('edit');
544 if (edit) {
545 var $general_comments = null;
546 var $inline_comments = $node.closest('div.inline-comments');
547 if (!$inline_comments.length) {
548 $general_comments = $('#comments');
549 var $comment = $general_comments.parent().find('div.comment:hidden');
550 // show hidden general comment form
551 $('#cb-comment-general-form-placeholder').show();
552 } else {
553 var $comment = $inline_comments.find('div.comment:hidden');
554 }
555 $comment.show();
556 }
486 557 $node.closest('.comment-inline-form').remove();
487 558 return false;
488 559 };
489 560
561 this.showVersion = function (comment_id, comment_history_id) {
562
563 var historyViewUrl = pyroutes.url(
564 'repo_commit_comment_history_view',
565 {
566 'repo_name': templateContext.repo_name,
567 'commit_id': comment_id,
568 'comment_history_id': comment_history_id,
569 }
570 );
571 successRenderCommit = function (data) {
572 SwalNoAnimation.fire({
573 html: data,
574 title: '',
575 });
576 };
577 failRenderCommit = function () {
578 SwalNoAnimation.fire({
579 html: 'Error while loading comment history',
580 title: '',
581 });
582 };
583 _submitAjaxPOST(
584 historyViewUrl, {'csrf_token': CSRF_TOKEN},
585 successRenderCommit,
586 failRenderCommit
587 );
588 };
589
490 590 this.getLineNumber = function(node) {
491 591 var $node = $(node);
492 592 var lineNo = $node.closest('td').attr('data-line-no');
@@ -638,12 +738,12 b' var CommentsController = function() {'
638 738 $node.closest('tr').toggleClass('hide-line-comments');
639 739 };
640 740
641 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
741 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
642 742 var pullRequestId = templateContext.pull_request_data.pull_request_id;
643 743 var commitId = templateContext.commit_data.commit_id;
644 744
645 745 var commentForm = new CommentForm(
646 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
746 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
647 747 var cm = commentForm.getCmInstance();
648 748
649 749 if (resolvesCommentId){
@@ -780,18 +880,234 b' var CommentsController = function() {'
780 880
781 881 var _form = $($form[0]);
782 882 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
883 var edit = false;
884 var comment_id = null;
783 885 var commentForm = this.createCommentForm(
784 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
886 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
785 887 commentForm.initStatusChangeSelector();
786 888
787 889 return commentForm;
788 890 };
789 891
892 this.editComment = function(node) {
893 var $node = $(node);
894 var $comment = $(node).closest('.comment');
895 var comment_id = $comment.attr('data-comment-id');
896 var $form = null
897
898 var $comments = $node.closest('div.inline-comments');
899 var $general_comments = null;
900 var lineno = null;
901
902 if($comments.length){
903 // inline comments setup
904 $form = $comments.find('.comment-inline-form');
905 lineno = self.getLineNumber(node)
906 }
907 else{
908 // general comments setup
909 $comments = $('#comments');
910 $form = $comments.find('.comment-inline-form');
911 lineno = $comment[0].id
912 $('#cb-comment-general-form-placeholder').hide();
913 }
914
915 this.edit = true;
916
917 if (!$form.length) {
918
919 var $filediff = $node.closest('.filediff');
920 $filediff.removeClass('hide-comments');
921 var f_path = $filediff.attr('data-f-path');
922
923 // create a new HTML from template
924
925 var tmpl = $('#cb-comment-inline-form-template').html();
926 tmpl = tmpl.format(escapeHtml(f_path), lineno);
927 $form = $(tmpl);
928 $comment.after($form)
929
930 var _form = $($form[0]).find('form');
931 var autocompleteActions = ['as_note',];
932 var commentForm = this.createCommentForm(
933 _form, lineno, '', autocompleteActions, resolvesCommentId,
934 this.edit, comment_id);
935 var old_comment_text_binary = $comment.attr('data-comment-text');
936 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
937 commentForm.cm.setValue(old_comment_text);
938 $comment.hide();
939
940 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
941 form: _form,
942 parent: $comments,
943 lineno: lineno,
944 f_path: f_path}
945 );
946
947 // set a CUSTOM submit handler for inline comments.
948 commentForm.setHandleFormSubmit(function(o) {
949 var text = commentForm.cm.getValue();
950 var commentType = commentForm.getCommentType();
951
952 if (text === "") {
953 return;
954 }
955
956 if (old_comment_text == text) {
957 SwalNoAnimation.fire({
958 title: 'Unable to edit comment',
959 html: _gettext('Comment body was not changed.'),
960 });
961 return;
962 }
963 var excludeCancelBtn = false;
964 var submitEvent = true;
965 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
966 commentForm.cm.setOption("readOnly", true);
967
968 // Read last version known
969 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
970 var version = versionSelector.data('lastVersion');
971
972 if (!version) {
973 version = 0;
974 }
975
976 var postData = {
977 'text': text,
978 'f_path': f_path,
979 'line': lineno,
980 'comment_type': commentType,
981 'version': version,
982 'csrf_token': CSRF_TOKEN
983 };
984
985 var submitSuccessCallback = function(json_data) {
986 $form.remove();
987 $comment.show();
988 var postData = {
989 'text': text,
990 'renderer': $comment.attr('data-comment-renderer'),
991 'csrf_token': CSRF_TOKEN
992 };
993
994 /* Inject new edited version selector */
995 var updateCommentVersionDropDown = function () {
996 var versionSelectId = '#comment_versions_'+comment_id;
997 var preLoadVersionData = [
998 {
999 id: json_data['comment_version'],
1000 text: "v{0}".format(json_data['comment_version']),
1001 action: function () {
1002 Rhodecode.comments.showVersion(
1003 json_data['comment_id'],
1004 json_data['comment_history_id']
1005 )
1006 },
1007 comment_version: json_data['comment_version'],
1008 comment_author_username: json_data['comment_author_username'],
1009 comment_author_gravatar: json_data['comment_author_gravatar'],
1010 comment_created_on: json_data['comment_created_on'],
1011 },
1012 ]
1013
1014
1015 if ($(versionSelectId).data('select2')) {
1016 var oldData = $(versionSelectId).data('select2').opts.data.results;
1017 $(versionSelectId).select2("destroy");
1018 preLoadVersionData = oldData.concat(preLoadVersionData)
1019 }
1020
1021 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1022
1023 $comment.attr('data-comment-text', utf8ToB64(text));
1024
1025 var versionSelector = $('#comment_versions_'+comment_id);
1026
1027 // set lastVersion so we know our last edit version
1028 versionSelector.data('lastVersion', json_data['comment_version'])
1029 versionSelector.parent().show();
1030 }
1031 updateCommentVersionDropDown();
1032
1033 // by default we reset state of comment preserving the text
1034 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1035 var prefix = "Error while editing this comment.\n"
1036 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1037 ajaxErrorSwal(message);
1038 };
1039
1040 var successRenderCommit = function(o){
1041 $comment.show();
1042 $comment[0].lastElementChild.innerHTML = o;
1043 };
1044
1045 var previewUrl = pyroutes.url(
1046 'repo_commit_comment_preview',
1047 {'repo_name': templateContext.repo_name,
1048 'commit_id': templateContext.commit_data.commit_id});
1049
1050 _submitAjaxPOST(
1051 previewUrl, postData, successRenderCommit,
1052 failRenderCommit
1053 );
1054
1055 try {
1056 var html = json_data.rendered_text;
1057 var lineno = json_data.line_no;
1058 var target_id = json_data.target_id;
1059
1060 $comments.find('.cb-comment-add-button').before(html);
1061
1062 // run global callback on submit
1063 commentForm.globalSubmitSuccessCallback();
1064
1065 } catch (e) {
1066 console.error(e);
1067 }
1068
1069 // re trigger the linkification of next/prev navigation
1070 linkifyComments($('.inline-comment-injected'));
1071 timeagoActivate();
1072 tooltipActivate();
1073
1074 if (window.updateSticky !== undefined) {
1075 // potentially our comments change the active window size, so we
1076 // notify sticky elements
1077 updateSticky()
1078 }
1079
1080 commentForm.setActionButtonsDisabled(false);
1081
1082 };
1083 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1084 var prefix = "Error while editing comment.\n"
1085 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1086 if (jqXHR.status == 409){
1087 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1088 ajaxErrorSwal(message, 'Comment version mismatch.');
1089 } else {
1090 ajaxErrorSwal(message);
1091 }
1092
1093 commentForm.resetCommentFormState(text)
1094 };
1095 commentForm.submitAjaxPOST(
1096 commentForm.submitUrl, postData,
1097 submitSuccessCallback,
1098 submitFailCallback);
1099 });
1100 }
1101
1102 $form.addClass('comment-inline-form-open');
1103 };
1104
790 1105 this.createComment = function(node, resolutionComment) {
791 1106 var resolvesCommentId = resolutionComment || null;
792 1107 var $node = $(node);
793 1108 var $td = $node.closest('td');
794 1109 var $form = $td.find('.comment-inline-form');
1110 this.edit = false;
795 1111
796 1112 if (!$form.length) {
797 1113
@@ -816,8 +1132,9 b' var CommentsController = function() {'
816 1132 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
817 1133 var _form = $($form[0]).find('form');
818 1134 var autocompleteActions = ['as_note', 'as_todo'];
1135 var comment_id=null;
819 1136 var commentForm = this.createCommentForm(
820 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
1137 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
821 1138
822 1139 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
823 1140 form: _form,
@@ -70,7 +70,7 b" replacing '-' and '_' into spaces"
70 70 * @param limit
71 71 * @returns {*[]}
72 72 */
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
73 var getTitleAndDescription = function(sourceRefType, sourceRef, elements, limit) {
74 74 var title = '';
75 75 var desc = '';
76 76
@@ -85,7 +85,9 b' var getTitleAndDescription = function(so'
85 85 }
86 86 else {
87 87 // use reference name
88 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 var normalizedRef = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter()
89 var refType = sourceRefType;
90 title = 'Changes from {0}: {1}'.format(refType, normalizedRef);
89 91 }
90 92
91 93 return [title, desc]
@@ -130,10 +130,13 b' function formatErrorMessage(jqXHR, textS'
130 130 }
131 131 }
132 132
133 function ajaxErrorSwal(message) {
133 function ajaxErrorSwal(message, title) {
134
135 var title = (typeof title !== 'undefined') ? title : _gettext('Ajax Request Error');
136
134 137 SwalNoAnimation.fire({
135 138 icon: 'error',
136 title: _gettext('Ajax Request Error'),
139 title: title,
137 140 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
138 141 showClass: {
139 142 popup: 'swal2-noanimation',
@@ -182,3 +182,13 b' var htmlEnDeCode = (function() {'
182 182 htmlDecode: htmlDecode
183 183 };
184 184 })();
185
186 function b64DecodeUnicode(str) {
187 return decodeURIComponent(atob(str).split('').map(function (c) {
188 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
189 }).join(''));
190 }
191
192 function utf8ToB64( str ) {
193 return window.btoa(unescape(encodeURIComponent( str )));
194 } No newline at end of file
@@ -11,9 +11,15 b''
11 11 <div class="panel-body">
12 12 <div class="apikeys_wrap">
13 13 <p>
14 ${_('Authentication tokens can be used to interact with the API, or VCS-over-http. '
15 'Each token can have a role. Token with a role can be used only in given context, '
16 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
14 ${_('Available roles')}:
15 <ul>
16 % for role in h.UserApiKeys.ROLES:
17 <li>
18 <span class="tag disabled">${h.UserApiKeys._get_role_name(role)}</span>
19 <span>${h.UserApiKeys._get_role_description(role) |n}</span>
20 </li>
21 % endfor
22 </ul>
17 23 </p>
18 24 <table class="rctable auth_tokens">
19 25 <tr>
@@ -36,7 +42,7 b''
36 42 </td>
37 43 <td class="td-wrap">${auth_token.description}</td>
38 44 <td class="td-tags">
39 <span class="tag disabled">${auth_token.role_humanized}</span>
45 <span class="tooltip tag disabled" title="${h.UserApiKeys._get_role_description(auth_token.role)}">${auth_token.role_humanized}</span>
40 46 </td>
41 47 <td class="td">${auth_token.scope_humanized}</td>
42 48 <td class="td-exp">
@@ -45,6 +45,11 b''
45 45 </div>
46 46 <div class="select">
47 47 ${h.select('group_parent_id',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
48 % if c.personal_repo_group:
49 <a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
50 ${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}
51 </a>
52 % endif
48 53 </div>
49 54 </div>
50 55
@@ -106,6 +111,12 b''
106 111 setCopyPermsOption(e.val)
107 112 });
108 113 $('#group_name').focus();
114
115 $('#select_my_group').on('click', function(e){
116 e.preventDefault();
117 $("#group_parent_id").val($(this).data('personalGroupId')).trigger("change");
118 })
119
109 120 })
110 121 </script>
111 122 </%def>
@@ -68,9 +68,14 b''
68 68 <span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
69 69 % endif
70 70 % else:
71 % if getattr(_user, 'duplicate_perm', None):
72 <span class="user-perm-duplicate">
71 73 ${h.link_to_user(_user.username)}
72 %if getattr(_user, 'duplicate_perm', None):
73 (${_('inactive duplicate')})
74 <span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
75 </span>
76 </span>
77 % else:
78 ${h.link_to_user(_user.username)}
74 79 %endif
75 80 % endif
76 81 </span>
@@ -116,9 +121,14 b''
116 121 <span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
117 122 % endif
118 123 % else:
124 % if getattr(_user, 'duplicate_perm', None):
125 <span class="user-perm-duplicate">
119 126 ${h.link_to_user(_user.username)}
120 %if getattr(_user, 'duplicate_perm', None):
121 (${_('inactive duplicate')})
127 <span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
128 </span>
129 </span>
130 % else:
131 ${h.link_to_user(_user.username)}
122 132 %endif
123 133 % endif
124 134 <span class="user-perm-help-text">(${_('delegated admin')})</span>
@@ -46,7 +46,7 b''
46 46 ${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
47 47 % if c.personal_repo_group:
48 48 <a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
49 ${_('Select my personal group (%(repo_group_name)s)') % {'repo_group_name': c.personal_repo_group.group_name}}
49 ${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}
50 50 </a>
51 51 % endif
52 52 <span class="help-block">${_('Optionally select a group to put this repository into.')}</span>
@@ -167,11 +167,16 b''
167 167 <div style="margin: 0 0 20px 0" class="fake-space"></div>
168 168
169 169 <div class="field">
170 % if c.rhodecode_db_repo.archived:
171 This repository is already archived. Only super-admin users can un-archive this repository.
172 % else:
170 173 <button class="btn btn-small btn-warning" type="submit"
171 174 onclick="submitConfirm(event, this, _gettext('Confirm to archive this repository'), _gettext('Archive'), '${c.rhodecode_db_repo.repo_name}')"
172 175 >
173 176 ${_('Archive this repository')}
174 177 </button>
178 % endif
179
175 180 </div>
176 181 <div class="field">
177 182 <span class="help-block">
@@ -94,10 +94,16 b''
94 94 <span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
95 95 % endif
96 96 % else:
97 % if getattr(_user, 'duplicate_perm', None):
98 <span class="user-perm-duplicate">
97 99 ${h.link_to_user(_user.username)}
98 %if getattr(_user, 'duplicate_perm', None):
99 (${_('inactive duplicate')})
100 <span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
101 </span>
102 </span>
103 % else:
104 ${h.link_to_user(_user.username)}
100 105 %endif
106
101 107 %if getattr(_user, 'branch_rules', None):
102 108 % if used_by_n_rules == 1:
103 109 (${_('used by {} branch rule, requires write+ permissions').format(used_by_n_rules)})
@@ -74,9 +74,14 b''
74 74 <span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
75 75 % endif
76 76 % else:
77 % if getattr(_user, 'duplicate_perm', None):
78 <span class="user-perm-duplicate">
77 79 ${h.link_to_user(_user.username)}
78 %if getattr(_user, 'duplicate_perm', None):
79 (${_('inactive duplicate')})
80 <span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
81 </span>
82 </span>
83 % else:
84 ${h.link_to_user(_user.username)}
80 85 %endif
81 86 % endif
82 87 </span>
@@ -122,9 +127,14 b''
122 127 <span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
123 128 % endif
124 129 % else:
130 % if getattr(_user, 'duplicate_perm', None):
131 <span class="user-perm-duplicate">
125 132 ${h.link_to_user(_user.username)}
126 %if getattr(_user, 'duplicate_perm', None):
127 (${_('inactive duplicate')})
133 <span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
134 </span>
135 </span>
136 % else:
137 ${h.link_to_user(_user.username)}
128 138 %endif
129 139 % endif
130 140 <span class="user-perm-help-text">(${_('delegated admin')})</span>
@@ -27,8 +27,8 b''
27 27 <%def name="main()">
28 28 <div class="box user_settings">
29 29 % if not c.user.active:
30 <div class="alert alert-warning text-center">
31 <strong>${_('This user is set as disabled')}</strong>
30 <div class="alert alert-warning text-center" style="margin: 0 0 15px 0">
31 <strong>${_('This user is set as non-active and disabled.')}</strong>
32 32 </div>
33 33 % endif
34 34
@@ -16,9 +16,15 b''
16 16 <div class="panel-body">
17 17 <div class="apikeys_wrap">
18 18 <p>
19 ${_('Authentication tokens can be used to interact with the API, or VCS-over-http. '
20 'Each token can have a role. Token with a role can be used only in given context, '
21 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
19 ${_('Available roles')}:
20 <ul>
21 % for role in h.UserApiKeys.ROLES:
22 <li>
23 <span class="tag disabled">${h.UserApiKeys._get_role_name(role)}</span>
24 <span>${h.UserApiKeys._get_role_description(role) |n}</span>
25 </li>
26 % endfor
27 </ul>
22 28 </p>
23 29 <table class="rctable auth_tokens">
24 30 <tr>
@@ -41,7 +47,7 b''
41 47 </td>
42 48 <td class="td-wrap">${auth_token.description}</td>
43 49 <td class="td-tags">
44 <span class="tag disabled">${auth_token.role_humanized}</span>
50 <span class="tooltip tag disabled" title="${h.UserApiKeys._get_role_description(auth_token.role)}">${auth_token.role_humanized}</span>
45 51 </td>
46 52 <td class="td">${auth_token.scope_humanized}</td>
47 53 <td class="td-exp">
@@ -1,11 +1,7 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%!
4 ## base64 filter e.g ${ example | base64 }
5 def base64(text):
6 import base64
7 from rhodecode.lib.helpers import safe_str
8 return base64.encodestring(safe_str(text))
4 from rhodecode.lib import html_filters
9 5 %>
10 6
11 7 <%inherit file="root.mako"/>
@@ -247,7 +243,9 b''
247 243
248 244 <div class="${_class}">
249 245 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
250 <span class="${('user user-disabled' if show_disabled else 'user')}"> ${h.link_to_user(rc_user or contact)}</span>
246 <span class="${('user user-disabled' if show_disabled else 'user')}">
247 ${h.link_to_user(rc_user or contact)}
248 </span>
251 249 </div>
252 250 </%def>
253 251
@@ -396,7 +394,7 b''
396 394 </a>
397 395 </li>
398 396
399 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
397 %if not c.rhodecode_db_repo.archived and h.HasRepoPermissionAll('repository.admin')(c.repo_name):
400 398 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
401 399 %endif
402 400
@@ -510,7 +508,7 b''
510 508 ## create action
511 509 <li>
512 510 <a href="#create-actions" onclick="return false;" class="menulink childs">
513 <i class="tooltip icon-plus-circled" title="${_('Create')}"></i>
511 <i class="icon-plus-circled"></i>
514 512 </a>
515 513
516 514 <div class="action-menu submenu">
@@ -1132,6 +1130,19 b''
1132 1130 };
1133 1131 ajaxPOST(url, postData, success, failure);
1134 1132 }
1133
1134 var hideLicenseWarning = function () {
1135 var fingerprint = templateContext.session_attrs.license_fingerprint;
1136 storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint);
1137 $('#notifications').hide();
1138 }
1139
1140 var hideLicenseError = function () {
1141 var fingerprint = templateContext.session_attrs.license_fingerprint;
1142 storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint);
1143 $('#notifications').hide();
1144 }
1145
1135 1146 </script>
1136 1147 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1137 1148 </%def>
@@ -3,6 +3,7 b''
3 3 ## <%namespace name="dpb" file="/base/default_perms_box.mako"/>
4 4 ## ${dpb.default_perms_box(<url_to_form>)}
5 5 ## ${dpb.default_perms_radios()}
6 <%namespace name="base" file="/base/base.mako"/>
6 7
7 8 <%def name="default_perms_radios(global_permissions_template = False, suffix='', **kwargs)">
8 9 <div class="main-content-full-width">
@@ -11,10 +12,22 b''
11 12 ## displayed according to checkbox selection
12 13 <div class="panel-heading">
13 14 %if not global_permissions_template:
14 <h3 class="inherit_overlay_default panel-title">${_('Inherited Permissions')}</h3>
15 <h3 class="inherit_overlay panel-title">${_('Custom Permissions')}</h3>
15 <h3 class="inherit_overlay_default panel-title">
16 % if hasattr(c, 'user'):
17 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')} &nbsp;-
18 % endif
19 ${_('Inherited Permissions')}
20 </h3>
21 <h3 class="inherit_overlay panel-title">
22 % if hasattr(c, 'user'):
23 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')} &nbsp;-
24 % endif
25 ${_('Custom Permissions')}
26 </h3>
16 27 %else:
17 <h3 class="panel-title">${_('Default Global Permissions')}</h3>
28 <h3 class="panel-title">
29 ${_('Default Global Permissions')}
30 </h3>
18 31 %endif
19 32 </div>
20 33
@@ -2,14 +2,15 b''
2 2 ## usage:
3 3 ## <%namespace name="p" file="/base/perms_summary.mako"/>
4 4 ## ${p.perms_summary(c.perm_user.permissions)}
5 <%namespace name="base" file="/base/base.mako"/>
5 6
6 7 <%def name="perms_summary(permissions, show_all=False, actions=True, side_link=None)">
7 8 <% section_to_label = {
8 'global': 'Global Permissions',
9 'repository_branches': 'Repository Branch Rules',
10 'repositories': 'Repository Access Permissions',
11 'user_groups': 'User Group Permissions',
12 'repositories_groups': 'Repository Group Permissions',
9 'global': 'Global Permissions Summary',
10 'repository_branches': 'Repository Branch Rules Summary',
11 'repositories': 'Repository Access Permissions Summary',
12 'user_groups': 'User Group Permissions Summary',
13 'repositories_groups': 'Repository Group Permissions Summary',
13 14 } %>
14 15
15 16 <div id="perms" class="table fields">
@@ -18,7 +19,11 b''
18 19
19 20 <div class="panel panel-default">
20 21 <div class="panel-heading" id="${section.replace("_","-")}-permissions">
21 <h3 class="panel-title">${section_to_label.get(section, section)} - <span id="total_count_${section}"></span>
22 <h3 class="panel-title">
23 % if hasattr(c, 'user'):
24 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')} &nbsp;-
25 % endif
26 ${section_to_label.get(section, section)} - <span id="total_count_${section}"></span>
22 27 <a class="permalink" href="#${section.replace("_","-")}-permissions"> ΒΆ</a>
23 28 </h3>
24 29 % if side_link:
@@ -158,7 +158,7 b''
158 158
159 159 <div class="cs_files">
160 160 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
161 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id])}
161 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id], commit=c.commit)}
162 162 ${cbdiffs.render_diffset(
163 163 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,inline_comments=c.inline_comments )}
164 164 </div>
@@ -3,8 +3,12 b''
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6
7 <%!
8 from rhodecode.lib import html_filters
9 %>
10
6 11 <%namespace name="base" file="/base/base.mako"/>
7
8 12 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
9 13 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 14 <% latest_ver = len(getattr(c, 'versions', [])) %>
@@ -21,113 +25,170 b''
21 25 line="${comment.line_no}"
22 26 data-comment-id="${comment.comment_id}"
23 27 data-comment-type="${comment.comment_type}"
28 data-comment-renderer="${comment.renderer}"
29 data-comment-text="${comment.text | html_filters.base64,n}"
24 30 data-comment-line-no="${comment.line_no}"
25 31 data-comment-inline=${h.json.dumps(inline)}
26 32 style="${'display: none;' if outdated_at_ver else ''}">
27 33
28 34 <div class="meta">
29 35 <div class="comment-type-label">
30 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}" title="line: ${comment.line_no}">
36 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
37
38 ## TODO COMMENT
31 39 % if comment.comment_type == 'todo':
32 40 % if comment.resolved:
33 41 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
42 <i class="icon-flag-filled"></i>
34 43 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
35 44 </div>
36 45 % else:
37 46 <div class="resolved tooltip" style="display: none">
38 47 <span>${comment.comment_type}</span>
39 48 </div>
40 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
49 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
50 <i class="icon-flag-filled"></i>
41 51 ${comment.comment_type}
42 52 </div>
43 53 % endif
54 ## NOTE COMMENT
44 55 % else:
56 ## RESOLVED NOTE
45 57 % if comment.resolved_comment:
58 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
46 59 fix
47 60 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
48 61 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
49 62 </a>
63 </div>
64 ## STATUS CHANGE NOTE
65 % elif not comment.is_inline and comment.status_change:
66 <%
67 if comment.pull_request:
68 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
69 else:
70 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
71 %>
72
73 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
74 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
75 ${comment.status_change[0].status_lbl}
76 </div>
50 77 % else:
51 ${comment.comment_type or 'note'}
78 <div>
79 <i class="icon-comment"></i>
80 ${(comment.comment_type or 'note')}
81 </div>
52 82 % endif
53 83 % endif
84
54 85 </div>
55 86 </div>
56 87
88 % if 0 and comment.status_change:
89 <div class="pull-left">
90 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
91 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
92 ${'!{}'.format(comment.pull_request.pull_request_id)}
93 </a>
94 </span>
95 </div>
96 % endif
97
57 98 <div class="author ${'author-inline' if inline else 'author-general'}">
58 99 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
59 100 </div>
101
60 102 <div class="date">
61 103 ${h.age_component(comment.modified_at, time_is_local=True)}
62 104 </div>
63 % if inline:
64 <span></span>
65 % else:
66 <div class="status-change">
67 % if comment.pull_request:
68 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
69 % if comment.status_change:
70 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}:
71 % else:
72 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}
73 % endif
74 </a>
75 % else:
76 % if comment.status_change:
77 ${_('Status change on commit')}:
78 % endif
79 % endif
80 </div>
105
106 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
107 <span class="tag authortag tooltip" title="${_('Pull request author')}">
108 ${_('author')}
109 </span>
81 110 % endif
82 111
83 % if comment.status_change:
84 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
85 <div title="${_('Commit status')}" class="changeset-status-lbl">
86 ${comment.status_change[0].status_lbl}
112 <%
113 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
114 %>
115
116 % if comment.history:
117 <div class="date">
118
119 <input id="${comment_version_selector}" name="${comment_version_selector}"
120 type="hidden"
121 data-last-version="${comment.history[-1].version}">
122
123 <script type="text/javascript">
124
125 var preLoadVersionData = [
126 % for comment_history in comment.history:
127 {
128 id: ${comment_history.comment_history_id},
129 text: 'v${comment_history.version}',
130 action: function () {
131 Rhodecode.comments.showVersion(
132 "${comment.comment_id}",
133 "${comment_history.comment_history_id}"
134 )
135 },
136 comment_version: "${comment_history.version}",
137 comment_author_username: "${comment_history.author.username}",
138 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
139 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
140 },
141 % endfor
142 ]
143 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
144
145 </script>
146
147 </div>
148 % else:
149 <div class="date" style="display: none">
150 <input id="${comment_version_selector}" name="${comment_version_selector}"
151 type="hidden"
152 data-last-version="0">
87 153 </div>
88 154 % endif
89 155
90 156 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
91 157
92 158 <div class="comment-links-block">
93 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
94 <span class="tag authortag tooltip" title="${_('Pull request author')}">
95 ${_('author')}
96 </span>
97 |
98 % endif
159
99 160 % if inline:
100 <div class="pr-version-inline">
101 <a href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
161 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
102 162 % if outdated_at_ver:
103 <code class="pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
163 <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
104 164 outdated ${'v{}'.format(pr_index_ver)} |
105 165 </code>
106 166 % elif pr_index_ver:
107 <code class="pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
167 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
108 168 ${'v{}'.format(pr_index_ver)} |
109 169 </code>
110 170 % endif
111 171 </a>
112 </div>
113 172 % else:
114 % if comment.pull_request_version_id and pr_index_ver:
115 |
116 <div class="pr-version">
173 % if pr_index_ver:
174
117 175 % if comment.outdated:
118 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
176 <a class="pr-version"
177 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
178 >
119 179 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
120 </a>
180 </a> |
121 181 % else:
122 <div title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
123 <a 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)}">
182 <a class="tooltip pr-version"
183 title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}"
184 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)}"
185 >
124 186 <code class="pr-version-num">
125 187 ${'v{}'.format(pr_index_ver)}
126 188 </code>
127 </a>
128 </div>
189 </a> |
129 190 % endif
130 </div>
191
131 192 % endif
132 193 % endif
133 194
@@ -136,21 +197,25 b''
136 197 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
137 198 ## permissions to delete
138 199 %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):
139 ## TODO: dan: add edit comment here
140 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
200 <a onclick="return Rhodecode.comments.editComment(this);"
201 class="edit-comment">${_('Edit')}</a>
202 | <a onclick="return Rhodecode.comments.deleteComment(this);"
203 class="delete-comment">${_('Delete')}</a>
141 204 %else:
142 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
205 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
206 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
143 207 %endif
144 208 %else:
145 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
209 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
210 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
146 211 %endif
147 212
148 213 % if outdated_at_ver:
149 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
150 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
214 | <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>
215 | <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>
151 216 % else:
152 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
153 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
217 | <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
218 | <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
154 219 % endif
155 220
156 221 </div>
@@ -259,7 +259,10 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
259 259 %>
260 260 <div class="filediff-collapse-indicator icon-"></div>
261 261 <span class="pill-group pull-right" >
262 <span class="pill"><i class="icon-comment"></i> ${len(total_file_comments)}</span>
262 <span class="pill" op="comments">
263
264 <i class="icon-comment"></i> ${len(total_file_comments)}
265 </span>
263 266 </span>
264 267 ${diff_ops(filediff)}
265 268
@@ -311,6 +314,7 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
311 314 ${hunk.section_header}
312 315 </td>
313 316 </tr>
317
314 318 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
315 319 % endfor
316 320
@@ -654,21 +658,28 b' def get_comments_for(diff_type, comments'
654 658 %>
655 659
656 660 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
657 %for i, line in enumerate(hunk.sideside):
661
662 <% chunk_count = 1 %>
663 %for loop_obj, item in h.looper(hunk.sideside):
658 664 <%
665 line = item
666 i = loop_obj.index
667 prev_line = loop_obj.previous
659 668 old_line_anchor, new_line_anchor = None, None
660 669
661 670 if line.original.lineno:
662 671 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
663 672 if line.modified.lineno:
664 673 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
674
675 line_action = line.modified.action or line.original.action
676 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
665 677 %>
666 678
667 679 <tr class="cb-line">
668 680 <td class="cb-data ${action_class(line.original.action)}"
669 681 data-line-no="${line.original.lineno}"
670 682 >
671 <div>
672 683
673 684 <% line_old_comments = None %>
674 685 %if line.original.get_comment_args:
@@ -677,12 +688,11 b' def get_comments_for(diff_type, comments'
677 688 %if line_old_comments:
678 689 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
679 690 % if has_outdated:
680 <i class="tooltip" title="${_('comments including outdated, click to show them')}:${len(line_old_comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
691 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_old_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
681 692 % else:
682 <i class="tooltip" title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
693 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_old_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
683 694 % endif
684 695 %endif
685 </div>
686 696 </td>
687 697 <td class="cb-lineno ${action_class(line.original.action)}"
688 698 data-line-no="${line.original.lineno}"
@@ -718,11 +728,12 b' def get_comments_for(diff_type, comments'
718 728 <% line_new_comments = None%>
719 729 %endif
720 730 %if line_new_comments:
731
721 732 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
722 733 % if has_outdated:
723 <i class="tooltip" title="${_('comments including outdated, click to show them')}:${len(line_new_comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
734 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_new_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
724 735 % else:
725 <i class="tooltip" title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
736 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_new_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
726 737 % endif
727 738 %endif
728 739 </div>
@@ -747,6 +758,12 b' def get_comments_for(diff_type, comments'
747 758 %if use_comments and line.modified.lineno and line_new_comments:
748 759 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries)}
749 760 %endif
761 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
762 <div class="nav-chunk" style="visibility: hidden">
763 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
764 </div>
765 <% chunk_count +=1 %>
766 % endif
750 767 </td>
751 768 </tr>
752 769 %endfor
@@ -776,9 +793,9 b' def get_comments_for(diff_type, comments'
776 793 % if comments:
777 794 <% has_outdated = any([x.outdated for x in comments]) %>
778 795 % if has_outdated:
779 <i class="tooltip" title="${_('comments including outdated, click to show them')}:${len(comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
796 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
780 797 % else:
781 <i class="tooltip" title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
798 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
782 799 % endif
783 800 % endif
784 801 </div>
@@ -838,7 +855,7 b' def get_comments_for(diff_type, comments'
838 855 </button>
839 856 </%def>
840 857
841 <%def name="render_diffset_menu(diffset, range_diff_on=None)">
858 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
842 859 <% diffset_container_id = h.md5(diffset.target_ref) %>
843 860
844 861 <div id="diff-file-sticky" class="diffset-menu clearinner">
@@ -899,12 +916,33 b' def get_comments_for(diff_type, comments'
899 916 </div>
900 917 </div>
901 918 </div>
902 <div class="fpath-placeholder">
919 <div class="fpath-placeholder pull-left">
903 920 <i class="icon-file-text"></i>
904 921 <strong class="fpath-placeholder-text">
905 922 Context file:
906 923 </strong>
907 924 </div>
925 <div class="pull-right noselect">
926
927 %if commit:
928 <span>
929 <code>${h.show_id(commit)}</code>
930 </span>
931 %elif pull_request_menu and pull_request_menu.get('pull_request'):
932 <span>
933 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
934 </span>
935 %endif
936 % if commit or pull_request_menu:
937 <span id="diff_nav">Loading diff...:</span>
938 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
939 <i class="icon-angle-up"></i>
940 </span>
941 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
942 <i class="icon-angle-down"></i>
943 </span>
944 % endif
945 </div>
908 946 <div class="sidebar_inner_shadow"></div>
909 947 </div>
910 948 </div>
@@ -1027,10 +1065,86 b' def get_comments_for(diff_type, comments'
1027 1065 e.preventDefault();
1028 1066 });
1029 1067
1068 diffNavText = 'diff navigation:'
1069
1070 getCurrentChunk = function () {
1071
1072 var chunksAll = $('.nav-chunk').filter(function () {
1073 return $(this).parents('.filediff').prev().get(0).checked !== true
1074 })
1075 var chunkSelected = $('.nav-chunk.selected');
1076 var initial = false;
1077
1078 if (chunkSelected.length === 0) {
1079 // no initial chunk selected, we pick first
1080 chunkSelected = $(chunksAll.get(0));
1081 var initial = true;
1082 }
1083
1084 return {
1085 'all': chunksAll,
1086 'selected': chunkSelected,
1087 'initial': initial,
1088 }
1089 }
1090
1091 animateDiffNavText = function () {
1092 var $diffNav = $('#diff_nav')
1093
1094 var callback = function () {
1095 $diffNav.animate({'opacity': 1.00}, 200)
1096 };
1097 $diffNav.animate({'opacity': 0.15}, 200, callback);
1098 }
1099
1100 scrollToChunk = function (moveBy) {
1101 var chunk = getCurrentChunk();
1102 var all = chunk.all
1103 var selected = chunk.selected
1104
1105 var curPos = all.index(selected);
1106 var newPos = curPos;
1107 if (!chunk.initial) {
1108 var newPos = curPos + moveBy;
1109 }
1110
1111 var curElem = all.get(newPos);
1112
1113 if (curElem === undefined) {
1114 // end or back
1115 $('#diff_nav').html('no next diff element:')
1116 animateDiffNavText()
1117 return
1118 } else if (newPos < 0) {
1119 $('#diff_nav').html('no previous diff element:')
1120 animateDiffNavText()
1121 return
1122 } else {
1123 $('#diff_nav').html(diffNavText)
1124 }
1125
1126 curElem = $(curElem)
1127 var offset = 100;
1128 $(window).scrollTop(curElem.position().top - offset);
1129
1130 //clear selection
1131 all.removeClass('selected')
1132 curElem.addClass('selected')
1133 }
1134
1135 scrollToPrevChunk = function () {
1136 scrollToChunk(-1)
1137 }
1138 scrollToNextChunk = function () {
1139 scrollToChunk(1)
1140 }
1141
1030 1142 </script>
1031 1143 % endif
1032 1144
1033 1145 <script type="text/javascript">
1146 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1147
1034 1148 $(document).ready(function () {
1035 1149
1036 1150 var contextPrefix = _gettext('Context file: ');
@@ -1209,6 +1323,46 b' def get_comments_for(diff_type, comments'
1209 1323 $('.toggle-wide-diff').addClass('btn-active');
1210 1324 updateSticky();
1211 1325 }
1326
1327 // DIFF NAV //
1328
1329 // element to detect scroll direction of
1330 var $window = $(window);
1331
1332 // initialize last scroll position
1333 var lastScrollY = $window.scrollTop();
1334
1335 $window.on('resize scrollstop', {latency: 350}, function () {
1336 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1337
1338 // get current scroll position
1339 var currentScrollY = $window.scrollTop();
1340
1341 // determine current scroll direction
1342 if (currentScrollY > lastScrollY) {
1343 var y = 'down'
1344 } else if (currentScrollY !== lastScrollY) {
1345 var y = 'up';
1346 }
1347
1348 var pos = -1; // by default we use last element in viewport
1349 if (y === 'down') {
1350 pos = -1;
1351 } else if (y === 'up') {
1352 pos = 0;
1353 }
1354
1355 if (visibleChunks.length > 0) {
1356 $('.nav-chunk').removeClass('selected');
1357 $(visibleChunks.get(pos)).addClass('selected');
1358 }
1359
1360 // update last scroll position to current position
1361 lastScrollY = currentScrollY;
1362
1363 });
1364 $('#diff_nav').html(diffNavText);
1365
1212 1366 });
1213 1367 </script>
1214 1368
@@ -940,7 +940,7 b' with multiple lines</p>'
940 940 Commenting on line o80.
941 941 </div>
942 942 <div class="comment-help pull-right">
943 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
943 Comments parsed using <a href="http://docutils.sourceforge.io/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
944 944 </div>
945 945 <div style="clear: both"></div>
946 946 <textarea id="text_o80" name="text" class="comment-block-ta ac-input" autocomplete="off"></textarea>
@@ -735,7 +735,7 b''
735 735 Commenting on line {1}.
736 736 </div>
737 737 <div class="comment-help pull-right">
738 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
738 Comments parsed using <a href="http://docutils.sourceforge.io/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
739 739 </div>
740 740 <div style="clear: both"></div>
741 741 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
@@ -786,7 +786,7 b''
786 786 Create a comment on this Pull Request.
787 787 </div>
788 788 <div class="comment-help pull-right">
789 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
789 Comments parsed using <a href="http://docutils.sourceforge.io/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
790 790 </div>
791 791 <div style="clear: both"></div>
792 792 <textarea class="comment-block-ta" id="text" name="text"></textarea>
@@ -8,11 +8,6 b''
8 8 SUBJECT:
9 9 <pre>${c.subject}</pre>
10 10
11 HEADERS:
12 <pre>
13 ${c.headers}
14 </pre>
15
16 11 PLAINTEXT:
17 12 <pre>
18 13 ${c.email_body_plaintext|n}
@@ -130,6 +130,34 b' var CG = new ColorGenerator();'
130 130 </script>
131 131
132 132
133 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
134
135 <%
136 if (size > 16) {
137 var gravatar_class = 'gravatar gravatar-large';
138 } else {
139 var gravatar_class = 'gravatar';
140 }
141
142 %>
143
144 <%
145 if (show_disabled) {
146 var user_cls = 'user user-disabled';
147 } else {
148 var user_cls = 'user';
149 }
150
151 %>
152
153 <div style='line-height: 20px'>
154 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
155 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
156 </div>
157
158 </script>
159
160
133 161 </div>
134 162
135 163 <script>
@@ -68,9 +68,6 b' text_monospace = "\'Menlo\', \'Liberation M'
68 68
69 69 %>
70 70
71 ## headers we additionally can set for email
72 <%def name="headers()" filter="n,trim"></%def>
73
74 71 <%def name="plaintext_footer()" filter="trim">
75 72 ${_('This is a notification from RhodeCode.')} ${instance_url}
76 73 </%def>
@@ -15,20 +15,24 b' data = {'
15 15 'comment_id': comment_id,
16 16
17 17 'commit_id': h.show_id(commit),
18 'mention_prefix': '[mention] ' if mention else '',
18 19 }
20
21
22 if comment_file:
23 subject_template = email_comment_file_subject_template or \
24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}` in the `{repo_name}` repository').format(**data)
25 else:
26 if status_change:
27 subject_template = email_comment_status_change_subject_template or \
28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on commit `{commit_id}` in the `{repo_name}` repository').format(**data)
29 else:
30 subject_template = email_comment_subject_template or \
31 _('{mention_prefix}{user} left a {comment_type} on commit `{commit_id}` in the `{repo_name}` repository').format(**data)
19 32 %>
20 33
21 34
22 % if comment_file:
23 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`').format(**data)} ${_('in the `{repo_name}` repository').format(**data) |n}
24 % else:
25 % if status_change:
26 ${(_('[mention]') if mention else '')} ${_('[status: {status}] {user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
27 % else:
28 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
29 % endif
30 % endif
31
35 ${subject_template.format(**data) |n}
32 36 </%def>
33 37
34 38 ## PLAINTEXT VERSION OF BODY
@@ -16,20 +16,23 b' data = {'
16 16
17 17 'pr_title': pull_request.title,
18 18 'pr_id': pull_request.pull_request_id,
19 'mention_prefix': '[mention] ' if mention else '',
19 20 }
21
22 if comment_file:
23 subject_template = email_pr_comment_file_subject_template or \
24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data)
25 else:
26 if status_change:
27 subject_template = email_pr_comment_status_change_subject_template or \
28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
29 else:
30 subject_template = email_pr_comment_subject_template or \
31 _('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
20 32 %>
21 33
22 34
23 % if comment_file:
24 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data) |n}
25 % else:
26 % if status_change:
27 ${(_('[mention]') if mention else '')} ${_('[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
28 % else:
29 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
30 % endif
31 % endif
32
35 ${subject_template.format(**data) |n}
33 36 </%def>
34 37
35 38 ## PLAINTEXT VERSION OF BODY
@@ -10,9 +10,11 b' data = {'
10 10 'pr_id': pull_request.pull_request_id,
11 11 'pr_title': pull_request.title,
12 12 }
13
14 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
13 15 %>
14 16
15 ${_('{user} requested a pull request review. !{pr_id}: "{pr_title}"').format(**data) |n}
17 ${subject_template.format(**data) |n}
16 18 </%def>
17 19
18 20 ## PLAINTEXT VERSION OF BODY
@@ -10,9 +10,11 b' data = {'
10 10 'pr_id': pull_request.pull_request_id,
11 11 'pr_title': pull_request.title,
12 12 }
13
14 subject_template = email_pr_update_subject_template or _('{updating_user} updated pull request. !{pr_id}: "{pr_title}"')
13 15 %>
14 16
15 ${_('{updating_user} updated pull request. !{pr_id}: "{pr_title}"').format(**data) |n}
17 ${subject_template.format(**data) |n}
16 18 </%def>
17 19
18 20 ## PLAINTEXT VERSION OF BODY
@@ -5,10 +5,6 b''
5 5 Test "Subject" ${_('hello "world"')|n}
6 6 </%def>
7 7
8 <%def name="headers()" filter="n,trim">
9 X=Y
10 </%def>
11
12 8 ## plain text version of the email. Empty by default
13 9 <%def name="body_plaintext()" filter="n,trim">
14 10 Email Plaintext Body
@@ -17,14 +17,30 b''
17 17 </div>
18 18
19 19 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
20 <div>
21 <a class="btn btn-primary new-file" href="${h.route_path('repo_files_upload_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
20
21 <div class="new-file">
22 <div class="btn-group btn-group-actions">
23 <a class="btn btn-primary no-margin" href="${h.route_path('repo_files_add_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
24 ${_('Add File')}
25 </a>
26
27 <a class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more options')}">
28 <i class="icon-down"></i>
29 </a>
30
31 <div class="btn-action-switcher-container right-align">
32 <ul class="btn-action-switcher" role="menu" style="min-width: 200px">
33 <li>
34 <a class="action_button" href="${h.route_path('repo_files_upload_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
35 <i class="icon-upload"></i>
22 36 ${_('Upload File')}
23 37 </a>
24 <a class="btn btn-primary new-file" href="${h.route_path('repo_files_add_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
25 ${_('Add File')}
26 </a>
38 </li>
39 </ul>
27 40 </div>
41 </div>
42 </div>
43
28 44 % endif
29 45
30 46 % if c.enable_downloads:
@@ -61,7 +61,7 b''
61 61 ##${h.secure_form(form_url, id='eform', enctype="multipart/form-data", request=request)}
62 62 <div class="edit-file-fieldset">
63 63 <div class="path-items">
64 <ul>
64 <ul class="tooltip" title="Repository path to store uploaded files. To change it, navigate to different path and click upload from there.">
65 65 <li class="breadcrumb-path">
66 66 <div>
67 67 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
@@ -79,7 +79,7 b''
79 79 <div class="upload-form table">
80 80 <div>
81 81
82 <div class="dropzone-wrapper" id="file-uploader">
82 <div class="dropzone-wrapper" id="file-uploader" style="border: none; padding: 40px 0">
83 83 <div class="dropzone-pure">
84 84 <div class="dz-message">
85 85 <i class="icon-upload" style="font-size:36px"></i></br>
@@ -250,9 +250,9 b''
250 250 var added = data['stats'][0]
251 251 var deleted = data['stats'][1]
252 252 var commonAncestorId = data['ancestor'];
253
254 var prTitleAndDesc = getTitleAndDescription(
255 sourceRef()[1], commitElements, 5);
253 var _sourceRefType = sourceRef()[0];
254 var _sourceRefName = sourceRef()[1];
255 var prTitleAndDesc = getTitleAndDescription(_sourceRefType, _sourceRefName, commitElements, 5);
256 256
257 257 var title = prTitleAndDesc[0];
258 258 var proposedDescription = prTitleAndDesc[1];
@@ -471,13 +471,11 b''
471 471 </div>
472 472 </div>
473 473 </div>
474 % elif c.pr_merge_source_commit.changed:
474 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
475 475 <div class="box">
476 476 <div class="alert alert-info">
477 477 <div>
478 % if c.pr_merge_source_commit.changed:
479 478 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
480 % endif
481 479 </div>
482 480 </div>
483 481 </div>
@@ -514,12 +512,12 b''
514 512 ${_('Update commits')}
515 513 </a>
516 514
517 <a id="update_commits_switcher" class="tooltip btn btn-primary" style="margin-left: -1px" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
515 <a id="update_commits_switcher" class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
518 516 <i class="icon-down"></i>
519 517 </a>
520 518
521 <div class="btn-action-switcher-container" id="update-commits-switcher">
522 <ul class="btn-action-switcher" role="menu">
519 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
520 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
523 521 <li>
524 522 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
525 523 ${_('Force update commits')}
@@ -626,11 +624,12 b''
626 624
627 625 <%
628 626 pr_menu_data = {
629 'outdated_comm_count_ver': outdated_comm_count_ver
627 'outdated_comm_count_ver': outdated_comm_count_ver,
628 'pull_request': c.pull_request
630 629 }
631 630 %>
632 631
633 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
632 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
634 633
635 634 % if c.range_diff_on:
636 635 % for commit in c.commit_ranges:
@@ -97,7 +97,7 b''
97 97 <div class="right-content">
98 98 <div class="commit-info">
99 99 <div class="tags">
100 <% commit_rev = c.rhodecode_db_repo.changeset_cache.get('revision') %>
100 <% commit_rev = h.safe_int(c.rhodecode_db_repo.changeset_cache.get('revision'), 0) + 1 %>
101 101 % if c.rhodecode_repo:
102 102 ${refs_counters(
103 103 c.rhodecode_repo.branches,
@@ -184,12 +184,37 b''
184 184 ${h.link_to(_('Enable downloads'),h.route_path('edit_repo',repo_name=c.repo_name, _anchor='repo_enable_downloads'))}
185 185 % endif
186 186 % else:
187 <span class="enabled">
188 <a id="archive_link" class="btn btn-small" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name,fname='tip.zip')}">
189 tip.zip
187 <div class="enabled pull-left" style="margin-right: 10px">
188
189 <div class="btn-group btn-group-actions">
190 <a class="archive_link btn btn-small" data-ext=".zip" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+'.zip')}">
191 <i class="icon-download"></i>
192 ${c.rhodecode_db_repo.landing_ref_name}.zip
190 193 ## replaced by some JS on select
191 194 </a>
192 </span>
195
196 <a class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more download options')}">
197 <i class="icon-down"></i>
198 </a>
199
200 <div class="btn-action-switcher-container left-align">
201 <ul class="btn-action-switcher" role="menu" style="min-width: 200px">
202 % for a_type, content_type, extension in h.ARCHIVE_SPECS:
203 % if extension not in ['.zip']:
204 <li>
205
206 <a class="archive_link" data-ext="${extension}" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+extension)}">
207 <i class="icon-download"></i>
208 ${c.rhodecode_db_repo.landing_ref_name+extension}
209 </a>
210 </li>
211 % endif
212 % endfor
213 </ul>
214 </div>
215 </div>
216
217 </div>
193 218 ${h.hidden('download_options')}
194 219 % endif
195 220 </div>
@@ -76,8 +76,8 b''
76 76
77 77 var initialCommitData = {
78 78 id: null,
79 text: 'tip',
80 type: 'tag',
79 text: '${c.rhodecode_db_repo.landing_ref_name}',
80 type: '${c.rhodecode_db_repo.landing_ref_type}',
81 81 raw_id: null,
82 82 files_url: null
83 83 };
@@ -87,15 +87,22 b''
87 87 // on change of download options
88 88 $('#download_options').on('change', function(e) {
89 89 // format of Object {text: "v0.0.3", type: "tag", id: "rev"}
90 var ext = '.zip';
91 var selected_cs = e.added;
92 var fname = e.added.raw_id + ext;
93 var href = pyroutes.url('repo_archivefile', {'repo_name': templateContext.repo_name, 'fname':fname});
90 var selectedReference = e.added;
91 var ico = '<i class="icon-download"></i>';
92
93 $.each($('.archive_link'), function (key, val) {
94 var ext = $(this).data('ext');
95 var fname = selectedReference.raw_id + ext;
96 var href = pyroutes.url('repo_archivefile', {
97 'repo_name': templateContext.repo_name,
98 'fname': fname
99 });
94 100 // set new label
95 $('#archive_link').html('{0}{1}'.format(escapeHtml(e.added.text), ext));
101 $(this).html(ico + ' {0}{1}'.format(escapeHtml(e.added.text), ext));
102 // set new url to button,
103 $(this).attr('href', href)
104 });
96 105
97 // set new url to button,
98 $('#archive_link').attr('href', href)
99 106 });
100 107
101 108
@@ -28,6 +28,7 b' from rhodecode.events import ('
28 28 PullRequestCreateEvent,
29 29 PullRequestUpdateEvent,
30 30 PullRequestCommentEvent,
31 PullRequestCommentEditEvent,
31 32 PullRequestReviewEvent,
32 33 PullRequestMergeEvent,
33 34 PullRequestCloseEvent,
@@ -80,6 +81,21 b' def test_pullrequest_comment_events_seri'
80 81
81 82
82 83 @pytest.mark.backends("git", "hg")
84 def test_pullrequest_comment_edit_events_serialized(pr_util, config_stub):
85 pr = pr_util.create_pull_request()
86 comment = CommentsModel().get_comments(
87 pr.target_repo.repo_id, pull_request=pr)[0]
88 event = PullRequestCommentEditEvent(pr, comment)
89 data = event.as_dict()
90 assert data['name'] == PullRequestCommentEditEvent.name
91 assert data['repo']['repo_name'] == pr.target_repo.repo_name
92 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
93 assert data['pullrequest']['url']
94 assert data['pullrequest']['permalink_url']
95 assert data['comment']['text'] == comment.text
96
97
98 @pytest.mark.backends("git", "hg")
83 99 def test_close_pull_request_events(pr_util, user_admin, config_stub):
84 100 pr = pr_util.create_pull_request()
85 101
@@ -29,7 +29,8 b' from rhodecode.events.repo import ('
29 29 RepoPrePullEvent, RepoPullEvent,
30 30 RepoPrePushEvent, RepoPushEvent,
31 31 RepoPreCreateEvent, RepoCreateEvent,
32 RepoPreDeleteEvent, RepoDeleteEvent, RepoCommitCommentEvent,
32 RepoPreDeleteEvent, RepoDeleteEvent,
33 RepoCommitCommentEvent, RepoCommitCommentEditEvent
33 34 )
34 35
35 36
@@ -138,8 +139,32 b' def test_repo_commit_event(config_stub, '
138 139 'comment_type': 'comment_type',
139 140 'f_path': 'f_path',
140 141 'line_no': 'line_no',
142 'last_version': 0,
141 143 })
142 144 event = EventClass(repo=repo_stub, commit=commit, comment=comment)
143 145 data = event.as_dict()
144 146 assert data['commit']['commit_id']
145 147 assert data['comment']['comment_id']
148
149
150 @pytest.mark.parametrize('EventClass', [RepoCommitCommentEditEvent])
151 def test_repo_commit_edit_event(config_stub, repo_stub, EventClass):
152
153 commit = StrictAttributeDict({
154 'raw_id': 'raw_id',
155 'message': 'message',
156 'branch': 'branch',
157 })
158
159 comment = StrictAttributeDict({
160 'comment_id': 'comment_id',
161 'text': 'text',
162 'comment_type': 'comment_type',
163 'f_path': 'f_path',
164 'line_no': 'line_no',
165 'last_version': 0,
166 })
167 event = EventClass(repo=repo_stub, commit=commit, comment=comment)
168 data = event.as_dict()
169 assert data['commit']['commit_id']
170 assert data['comment']['comment_id']
@@ -433,7 +433,7 b' def test_permission_calculator_admin_per'
433 433
434 434 calculator = auth.PermissionCalculator(
435 435 user.user_id, {}, False, False, True, 'higherwin')
436 permissions = calculator._calculate_admin_permissions()
436 permissions = calculator._calculate_super_admin_permissions()
437 437
438 438 assert permissions['repositories_groups'][repo_group.group_name] == \
439 439 'group.admin'
@@ -35,15 +35,12 b' def test_get_template_obj(app, request_s'
35 35
36 36 def test_render_email(app, http_host_only_stub):
37 37 kwargs = {}
38 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
38 subject, body, body_plaintext = EmailNotificationModel().render_email(
39 39 EmailNotificationModel.TYPE_TEST, **kwargs)
40 40
41 41 # subject
42 42 assert subject == 'Test "Subject" hello "world"'
43 43
44 # headers
45 assert headers == 'X=Y'
46
47 44 # body plaintext
48 45 assert body_plaintext == 'Email Plaintext Body'
49 46
@@ -80,7 +77,7 b' def test_render_pr_email(app, user_admin'
80 77 'pull_request_url': 'http://localhost/pr1',
81 78 }
82 79
83 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
80 subject, body, body_plaintext = EmailNotificationModel().render_email(
84 81 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
85 82
86 83 # subject
@@ -133,7 +130,7 b' def test_render_pr_update_email(app, use'
133 130 'removed_files': file_changes.removed,
134 131 }
135 132
136 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
133 subject, body, body_plaintext = EmailNotificationModel().render_email(
137 134 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **kwargs)
138 135
139 136 # subject
@@ -188,7 +185,6 b' def test_render_comment_subject_no_newli'
188 185
189 186 'pull_request_url': 'http://code.rc.com/_pr/123'
190 187 }
191 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
192 email_type, **kwargs)
188 subject, body, body_plaintext = EmailNotificationModel().render_email(email_type, **kwargs)
193 189
194 190 assert '\n' not in subject
@@ -113,6 +113,7 b' class TestRepoGroupSchema(object):'
113 113 repo_group_owner=user_regular.username
114 114 ))
115 115
116 expected = 'Parent repository group `{}` does not exist'.format(
117 test_repo_group.group_name)
116 expected = 'You do not have the permissions to store ' \
117 'repository groups inside repository group `{}`'\
118 .format(test_repo_group.group_name)
118 119 assert excinfo.value.asdict()['repo_group'] == expected
@@ -560,6 +560,7 b' class Backend(object):'
560 560
561 561 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
562 562 _master_repo = None
563 _master_repo_path = ''
563 564 _commit_ids = {}
564 565
565 566 def __init__(self, alias, repo_name, test_name, test_repo_container):
@@ -624,6 +625,8 b' class Backend(object):'
624 625 Returns a commit map which maps from commit message to raw_id.
625 626 """
626 627 self._master_repo = self.create_repo(commits=commits)
628 self._master_repo_path = self._master_repo.repo_full_path
629
627 630 return self._commit_ids
628 631
629 632 def create_repo(
@@ -661,11 +664,10 b' class Backend(object):'
661 664 """
662 665 Make sure that repo contains all commits mentioned in `heads`
663 666 """
664 vcsmaster = self._master_repo.scm_instance()
665 667 vcsrepo = repo.scm_instance()
666 668 vcsrepo.config.clear_section('hooks')
667 669 commit_ids = [self._commit_ids[h] for h in heads]
668 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
670 vcsrepo.pull(self._master_repo_path, commit_ids=commit_ids)
669 671
670 672 def create_fork(self):
671 673 repo_to_fork = self.repo_name
General Comments 0
You need to be logged in to leave comments. Login now