##// END OF EJS Templates
pull-requests: change the naming from #NUM into !NUM....
dan -
r4039:6942c656 default
parent child Browse files
Show More
@@ -1,76 +1,76 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Internal settings for vcs-lib
23 23 """
24 24
25 25 # list of default encoding used in safe_unicode/safe_str methods
26 26 DEFAULT_ENCODINGS = ['utf8']
27 27
28 28
29 29 # Compatibility version when creating SVN repositories. None means newest.
30 30 # Other available options are: pre-1.4-compatible, pre-1.5-compatible,
31 31 # pre-1.6-compatible, pre-1.8-compatible
32 32 SVN_COMPATIBLE_VERSION = None
33 33
34 34 ALIASES = ['hg', 'git', 'svn']
35 35
36 36 BACKENDS = {
37 37 'hg': 'rhodecode.lib.vcs.backends.hg.MercurialRepository',
38 38 'git': 'rhodecode.lib.vcs.backends.git.GitRepository',
39 39 'svn': 'rhodecode.lib.vcs.backends.svn.SubversionRepository',
40 40 }
41 41
42 42
43 43 ARCHIVE_SPECS = [
44 44 ('tbz2', 'application/x-bzip2', 'tbz2'),
45 45 ('tbz2', 'application/x-bzip2', '.tar.bz2'),
46 46
47 47 ('tgz', 'application/x-gzip', '.tgz'),
48 48 ('tgz', 'application/x-gzip', '.tar.gz'),
49 49
50 50 ('zip', 'application/zip', '.zip'),
51 51 ]
52 52
53 53 HOOKS_PROTOCOL = None
54 54 HOOKS_DIRECT_CALLS = False
55 55 HOOKS_HOST = '127.0.0.1'
56 56
57 57
58 58 MERGE_MESSAGE_TMPL = (
59 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}\n\n '
59 u'Merge pull request !{pr_id} from {source_repo} {source_ref_name}\n\n '
60 60 u'{pr_title}')
61 61 MERGE_DRY_RUN_MESSAGE = 'dry_run_merge_message_from_rhodecode'
62 62 MERGE_DRY_RUN_USER = 'Dry-Run User'
63 63 MERGE_DRY_RUN_EMAIL = 'dry-run-merge@rhodecode.com'
64 64
65 65
66 66 def available_aliases():
67 67 """
68 68 Mercurial is required for the system to work, so in case vcs.backends does
69 69 not include it, we make sure it will be available internally
70 70 TODO: anderson: refactor vcs.backends so it won't be necessary, VCS server
71 71 should be responsible to dictate available backends.
72 72 """
73 73 aliases = ALIASES[:]
74 74 if 'hg' not in aliases:
75 75 aliases += ['hg']
76 76 return aliases
@@ -1,94 +1,94 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-body">
5 5 %if c.closed:
6 6 ${h.checkbox('show_closed',checked="checked", label=_('Show Closed Pull Requests'))}
7 7 %else:
8 8 ${h.checkbox('show_closed',label=_('Show Closed Pull Requests'))}
9 9 %endif
10 10 </div>
11 11 </div>
12 12
13 13 <div class="panel panel-default">
14 14 <div class="panel-heading">
15 15 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
16 16 </div>
17 17 <div class="panel-body panel-body-min-height">
18 18 <table id="pull_request_list_table" class="display"></table>
19 19 </div>
20 20 </div>
21 21
22 22 <script type="text/javascript">
23 23 $(document).ready(function() {
24 24
25 25 $('#show_closed').on('click', function(e){
26 26 if($(this).is(":checked")){
27 27 window.location = "${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}";
28 28 }
29 29 else{
30 30 window.location = "${h.route_path('my_account_pullrequests')}";
31 31 }
32 32 });
33 33
34 34 var $pullRequestListTable = $('#pull_request_list_table');
35 35
36 36 // participating object list
37 37 $pullRequestListTable.DataTable({
38 38 processing: true,
39 39 serverSide: true,
40 40 ajax: {
41 41 "url": "${h.route_path('my_account_pullrequests_data')}",
42 42 "data": function (d) {
43 43 d.closed = "${c.closed}";
44 44 }
45 45 },
46 46 dom: 'rtp',
47 47 pageLength: ${c.visual.dashboard_items},
48 48 order: [[ 2, "desc" ]],
49 49 columns: [
50 50 { data: {"_": "status",
51 51 "sort": "status"}, title: "", className: "td-status", orderable: false},
52 52 { data: {"_": "target_repo",
53 53 "sort": "target_repo"}, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false},
54 54 { data: {"_": "name",
55 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
55 "sort": "name_raw"}, title: "${_('Id')}", className: "td-componentname", "type": "num" },
56 { data: {"_": "title",
57 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
56 58 { data: {"_": "author",
57 59 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
58 { data: {"_": "title",
59 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
60 60 { data: {"_": "comments",
61 61 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
62 62 { data: {"_": "updated_on",
63 63 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
64 64 ],
65 65 language: {
66 66 paginate: DEFAULT_GRID_PAGINATION,
67 67 sProcessing: _gettext('loading...'),
68 68 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
69 69 },
70 70 "drawCallback": function( settings, json ) {
71 71 timeagoActivate();
72 72 tooltipActivate();
73 73 },
74 74 "createdRow": function ( row, data, index ) {
75 75 if (data['closed']) {
76 76 $(row).addClass('closed');
77 77 }
78 78 if (data['owned']) {
79 79 $(row).addClass('owned');
80 80 }
81 81 if (data['state'] !== 'created') {
82 82 $(row).addClass('state-' + data['state']);
83 83 }
84 84 }
85 85 });
86 86 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
87 87 $pullRequestListTable.css('opacity', 1);
88 88 });
89 89
90 90 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
91 91 $pullRequestListTable.css('opacity', 0.3);
92 92 });
93 93 });
94 94 </script>
@@ -1,420 +1,420 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.mako"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 10 <% latest_ver = len(getattr(c, 'versions', [])) %>
11 11 % if inline:
12 12 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
13 13 % else:
14 14 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
15 15 % endif
16 16
17 17
18 18 <div class="comment
19 19 ${'comment-inline' if inline else 'comment-general'}
20 20 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
21 21 id="comment-${comment.comment_id}"
22 22 line="${comment.line_no}"
23 23 data-comment-id="${comment.comment_id}"
24 24 data-comment-type="${comment.comment_type}"
25 25 data-comment-line-no="${comment.line_no}"
26 26 data-comment-inline=${h.json.dumps(inline)}
27 27 style="${'display: none;' if outdated_at_ver else ''}">
28 28
29 29 <div class="meta">
30 30 <div class="comment-type-label">
31 31 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}" title="line: ${comment.line_no}">
32 32 % if comment.comment_type == 'todo':
33 33 % if comment.resolved:
34 34 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
35 35 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
36 36 </div>
37 37 % else:
38 38 <div class="resolved tooltip" style="display: none">
39 39 <span>${comment.comment_type}</span>
40 40 </div>
41 41 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
42 42 ${comment.comment_type}
43 43 </div>
44 44 % endif
45 45 % else:
46 46 % if comment.resolved_comment:
47 47 fix
48 48 <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)})">
49 49 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
50 50 </a>
51 51 % else:
52 52 ${comment.comment_type or 'note'}
53 53 % endif
54 54 % endif
55 55 </div>
56 56 </div>
57 57
58 58 <div class="author ${'author-inline' if inline else 'author-general'}">
59 59 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
60 60 </div>
61 61 <div class="date">
62 62 ${h.age_component(comment.modified_at, time_is_local=True)}
63 63 </div>
64 64 % if inline:
65 65 <span></span>
66 66 % else:
67 67 <div class="status-change">
68 68 % if comment.pull_request:
69 69 <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)}">
70 70 % if comment.status_change:
71 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
71 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}:
72 72 % else:
73 ${_('pull request #%s') % comment.pull_request.pull_request_id}
73 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}
74 74 % endif
75 75 </a>
76 76 % else:
77 77 % if comment.status_change:
78 78 ${_('Status change on commit')}:
79 79 % endif
80 80 % endif
81 81 </div>
82 82 % endif
83 83
84 84 % if comment.status_change:
85 85 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
86 86 <div title="${_('Commit status')}" class="changeset-status-lbl">
87 87 ${comment.status_change[0].status_lbl}
88 88 </div>
89 89 % endif
90 90
91 91 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
92 92
93 93 <div class="comment-links-block">
94 94 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
95 95 <span class="tag authortag tooltip" title="${_('Pull request author')}">
96 96 ${_('author')}
97 97 </span>
98 98 |
99 99 % endif
100 100 % if inline:
101 101 <div class="pr-version-inline">
102 102 <a href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
103 103 % if outdated_at_ver:
104 104 <code class="pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
105 105 outdated ${'v{}'.format(pr_index_ver)} |
106 106 </code>
107 107 % elif pr_index_ver:
108 108 <code class="pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
109 109 ${'v{}'.format(pr_index_ver)} |
110 110 </code>
111 111 % endif
112 112 </a>
113 113 </div>
114 114 % else:
115 115 % if comment.pull_request_version_id and pr_index_ver:
116 116 |
117 117 <div class="pr-version">
118 118 % if comment.outdated:
119 119 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
120 120 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
121 121 </a>
122 122 % else:
123 123 <div title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
124 124 <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)}">
125 125 <code class="pr-version-num">
126 126 ${'v{}'.format(pr_index_ver)}
127 127 </code>
128 128 </a>
129 129 </div>
130 130 % endif
131 131 </div>
132 132 % endif
133 133 % endif
134 134
135 135 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
136 136 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
137 137 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
138 138 ## permissions to delete
139 139 %if c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
140 140 ## TODO: dan: add edit comment here
141 141 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
142 142 %else:
143 143 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
144 144 %endif
145 145 %else:
146 146 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
147 147 %endif
148 148
149 149 % if outdated_at_ver:
150 150 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
151 151 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
152 152 % else:
153 153 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
154 154 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
155 155 % endif
156 156
157 157 </div>
158 158 </div>
159 159 <div class="text">
160 160 ${h.render(comment.text, renderer=comment.renderer, mentions=True)}
161 161 </div>
162 162
163 163 </div>
164 164 </%def>
165 165
166 166 ## generate main comments
167 167 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
168 168 <div class="general-comments" id="comments">
169 169 %for comment in comments:
170 170 <div id="comment-tr-${comment.comment_id}">
171 171 ## only render comments that are not from pull request, or from
172 172 ## pull request and a status change
173 173 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
174 174 ${comment_block(comment)}
175 175 %endif
176 176 </div>
177 177 %endfor
178 178 ## to anchor ajax comments
179 179 <div id="injected_page_comments"></div>
180 180 </div>
181 181 </%def>
182 182
183 183
184 184 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
185 185
186 186 <div class="comments">
187 187 <%
188 188 if is_pull_request:
189 189 placeholder = _('Leave a comment on this Pull Request.')
190 190 elif is_compare:
191 191 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
192 192 else:
193 193 placeholder = _('Leave a comment on this Commit.')
194 194 %>
195 195
196 196 % if c.rhodecode_user.username != h.DEFAULT_USER:
197 197 <div class="js-template" id="cb-comment-general-form-template">
198 198 ## template generated for injection
199 199 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
200 200 </div>
201 201
202 202 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
203 203 ## inject form here
204 204 </div>
205 205 <script type="text/javascript">
206 206 var lineNo = 'general';
207 207 var resolvesCommentId = null;
208 208 var generalCommentForm = Rhodecode.comments.createGeneralComment(
209 209 lineNo, "${placeholder}", resolvesCommentId);
210 210
211 211 // set custom success callback on rangeCommit
212 212 % if is_compare:
213 213 generalCommentForm.setHandleFormSubmit(function(o) {
214 214 var self = generalCommentForm;
215 215
216 216 var text = self.cm.getValue();
217 217 var status = self.getCommentStatus();
218 218 var commentType = self.getCommentType();
219 219
220 220 if (text === "" && !status) {
221 221 return;
222 222 }
223 223
224 224 // we can pick which commits we want to make the comment by
225 225 // selecting them via click on preview pane, this will alter the hidden inputs
226 226 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
227 227
228 228 var commitIds = [];
229 229 $('#changeset_compare_view_content .compare_select').each(function(el) {
230 230 var commitId = this.id.replace('row-', '');
231 231 if ($(this).hasClass('hl') || !cherryPicked) {
232 232 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
233 233 commitIds.push(commitId);
234 234 } else {
235 235 $("input[data-commit-id='{0}']".format(commitId)).val('')
236 236 }
237 237 });
238 238
239 239 self.setActionButtonsDisabled(true);
240 240 self.cm.setOption("readOnly", true);
241 241 var postData = {
242 242 'text': text,
243 243 'changeset_status': status,
244 244 'comment_type': commentType,
245 245 'commit_ids': commitIds,
246 246 'csrf_token': CSRF_TOKEN
247 247 };
248 248
249 249 var submitSuccessCallback = function(o) {
250 250 location.reload(true);
251 251 };
252 252 var submitFailCallback = function(){
253 253 self.resetCommentFormState(text)
254 254 };
255 255 self.submitAjaxPOST(
256 256 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
257 257 });
258 258 % endif
259 259
260 260 </script>
261 261 % else:
262 262 ## form state when not logged in
263 263 <div class="comment-form ac">
264 264
265 265 <div class="comment-area">
266 266 <div class="comment-area-header">
267 267 <ul class="nav-links clearfix">
268 268 <li class="active">
269 269 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
270 270 </li>
271 271 <li class="">
272 272 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
273 273 </li>
274 274 </ul>
275 275 </div>
276 276
277 277 <div class="comment-area-write" style="display: block;">
278 278 <div id="edit-container">
279 279 <div style="padding: 40px 0">
280 280 ${_('You need to be logged in to leave comments.')}
281 281 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
282 282 </div>
283 283 </div>
284 284 <div id="preview-container" class="clearfix" style="display: none;">
285 285 <div id="preview-box" class="preview-box"></div>
286 286 </div>
287 287 </div>
288 288
289 289 <div class="comment-area-footer">
290 290 <div class="toolbar">
291 291 <div class="toolbar-text">
292 292 </div>
293 293 </div>
294 294 </div>
295 295 </div>
296 296
297 297 <div class="comment-footer">
298 298 </div>
299 299
300 300 </div>
301 301 % endif
302 302
303 303 <script type="text/javascript">
304 304 bindToggleButtons();
305 305 </script>
306 306 </div>
307 307 </%def>
308 308
309 309
310 310 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
311 311
312 312 ## comment injected based on assumption that user is logged in
313 313 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
314 314
315 315 <div class="comment-area">
316 316 <div class="comment-area-header">
317 317 <ul class="nav-links clearfix">
318 318 <li class="active">
319 319 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
320 320 </li>
321 321 <li class="">
322 322 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
323 323 </li>
324 324 <li class="pull-right">
325 325 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
326 326 % for val in c.visual.comment_types:
327 327 <option value="${val}">${val.upper()}</option>
328 328 % endfor
329 329 </select>
330 330 </li>
331 331 </ul>
332 332 </div>
333 333
334 334 <div class="comment-area-write" style="display: block;">
335 335 <div id="edit-container_${lineno_id}">
336 336 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
337 337 </div>
338 338 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
339 339 <div id="preview-box_${lineno_id}" class="preview-box"></div>
340 340 </div>
341 341 </div>
342 342
343 343 <div class="comment-area-footer comment-attachment-uploader">
344 344 <div class="toolbar">
345 345 <div class="toolbar-text">
346 346 ${(_('Comments parsed using %s syntax with %s, and %s actions support.') % (
347 347 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
348 348 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user')),
349 349 ('<span class="tooltip" title="%s">`/`</span>' % _('Start typing with / for certain actions to be triggered via text box.'))
350 350 )
351 351 )|n}
352 352 </div>
353 353
354 354 <div class="comment-attachment-text">
355 355 <div class="dropzone-text">
356 356 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
357 357 </div>
358 358 <div class="dropzone-upload" style="display:none">
359 359 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
360 360 </div>
361 361 </div>
362 362
363 363 ## comments dropzone template, empty on purpose
364 364 <div style="display: none" class="comment-attachment-uploader-template">
365 365 <div class="dz-file-preview" style="margin: 0">
366 366 <div class="dz-error-message"></div>
367 367 </div>
368 368 </div>
369 369
370 370 </div>
371 371 </div>
372 372 </div>
373 373
374 374 <div class="comment-footer">
375 375
376 376 % if review_statuses:
377 377 <div class="status_box">
378 378 <select id="change_status_${lineno_id}" name="changeset_status">
379 379 <option></option> ## Placeholder
380 380 % for status, lbl in review_statuses:
381 381 <option value="${status}" data-status="${status}">${lbl}</option>
382 382 %if is_pull_request and change_status and status in ('approved', 'rejected'):
383 383 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
384 384 %endif
385 385 % endfor
386 386 </select>
387 387 </div>
388 388 % endif
389 389
390 390 ## inject extra inputs into the form
391 391 % if form_extras and isinstance(form_extras, (list, tuple)):
392 392 <div id="comment_form_extras">
393 393 % for form_ex_el in form_extras:
394 394 ${form_ex_el|n}
395 395 % endfor
396 396 </div>
397 397 % endif
398 398
399 399 <div class="action-buttons">
400 400 ## inline for has a file, and line-number together with cancel hide button.
401 401 % if form_type == 'inline':
402 402 <input type="hidden" name="f_path" value="{0}">
403 403 <input type="hidden" name="line" value="${lineno_id}">
404 404 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
405 405 ${_('Cancel')}
406 406 </button>
407 407 % endif
408 408
409 409 % if form_type != 'inline':
410 410 <div class="action-buttons-extra"></div>
411 411 % endif
412 412
413 413 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
414 414
415 415 </div>
416 416 </div>
417 417
418 418 </form>
419 419
420 420 </%def> No newline at end of file
@@ -1,469 +1,469 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5
6 6 <%def name="metatags_help()">
7 7 <table>
8 8 <%
9 9 example_tags = [
10 10 ('state','[stable]'),
11 11 ('state','[stale]'),
12 12 ('state','[featured]'),
13 13 ('state','[dev]'),
14 14 ('state','[dead]'),
15 15 ('state','[deprecated]'),
16 16
17 17 ('label','[personal]'),
18 18 ('generic','[v2.0.0]'),
19 19
20 20 ('lang','[lang =&gt; JavaScript]'),
21 21 ('license','[license =&gt; LicenseName]'),
22 22
23 23 ('ref','[requires =&gt; RepoName]'),
24 24 ('ref','[recommends =&gt; GroupName]'),
25 25 ('ref','[conflicts =&gt; SomeName]'),
26 26 ('ref','[base =&gt; SomeName]'),
27 27 ('url','[url =&gt; [linkName](https://rhodecode.com)]'),
28 28 ('see','[see =&gt; http://rhodecode.com]'),
29 29 ]
30 30 %>
31 31 % for tag_type, tag in example_tags:
32 32 <tr>
33 33 <td>${tag|n}</td>
34 34 <td>${h.style_metatag(tag_type, tag)|n}</td>
35 35 </tr>
36 36 % endfor
37 37 </table>
38 38 </%def>
39 39
40 40 <%def name="render_description(description, stylify_metatags)">
41 41 <%
42 42 tags = []
43 43 if stylify_metatags:
44 44 tags, description = h.extract_metatags(description)
45 45 %>
46 46 % for tag_type, tag in tags:
47 47 ${h.style_metatag(tag_type, tag)|n,trim}
48 48 % endfor
49 49 <code style="white-space: pre-wrap">${description}</code>
50 50 </%def>
51 51
52 52 ## REPOSITORY RENDERERS
53 53 <%def name="quick_menu(repo_name)">
54 54 <i class="icon-more"></i>
55 55 <div class="menu_items_container hidden">
56 56 <ul class="menu_items">
57 57 <li>
58 58 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
59 59 <span>${_('Summary')}</span>
60 60 </a>
61 61 </li>
62 62 <li>
63 63 <a title="${_('Commits')}" href="${h.route_path('repo_commits',repo_name=repo_name)}">
64 64 <span>${_('Commits')}</span>
65 65 </a>
66 66 </li>
67 67 <li>
68 68 <a title="${_('Files')}" href="${h.route_path('repo_files:default_commit',repo_name=repo_name)}">
69 69 <span>${_('Files')}</span>
70 70 </a>
71 71 </li>
72 72 <li>
73 73 <a title="${_('Fork')}" href="${h.route_path('repo_fork_new',repo_name=repo_name)}">
74 74 <span>${_('Fork')}</span>
75 75 </a>
76 76 </li>
77 77 </ul>
78 78 </div>
79 79 </%def>
80 80
81 81 <%def name="repo_name(name,rtype,rstate,private,archived,fork_of,short_name=False,admin=False)">
82 82 <%
83 83 def get_name(name,short_name=short_name):
84 84 if short_name:
85 85 return name.split('/')[-1]
86 86 else:
87 87 return name
88 88 %>
89 89 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
90 90 ##NAME
91 91 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
92 92
93 93 ##TYPE OF REPO
94 94 %if h.is_hg(rtype):
95 95 <span title="${_('Mercurial repository')}"><i class="icon-hg" style="font-size: 14px;"></i></span>
96 96 %elif h.is_git(rtype):
97 97 <span title="${_('Git repository')}"><i class="icon-git" style="font-size: 14px"></i></span>
98 98 %elif h.is_svn(rtype):
99 99 <span title="${_('Subversion repository')}"><i class="icon-svn" style="font-size: 14px"></i></span>
100 100 %endif
101 101
102 102 ##PRIVATE/PUBLIC
103 103 %if private is True and c.visual.show_private_icon:
104 104 <i class="icon-lock" title="${_('Private repository')}"></i>
105 105 %elif private is False and c.visual.show_public_icon:
106 106 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
107 107 %else:
108 108 <span></span>
109 109 %endif
110 110 ${get_name(name)}
111 111 </a>
112 112 %if fork_of:
113 113 <a href="${h.route_path('repo_summary',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
114 114 %endif
115 115 %if rstate == 'repo_state_pending':
116 116 <span class="creation_in_progress tooltip" title="${_('This repository is being created in a background task')}">
117 117 (${_('creating...')})
118 118 </span>
119 119 %endif
120 120
121 121 </div>
122 122 </%def>
123 123
124 124 <%def name="repo_desc(description, stylify_metatags)">
125 125 <%
126 126 tags, description = h.extract_metatags(description)
127 127 %>
128 128
129 129 <div class="truncate-wrap">
130 130 % if stylify_metatags:
131 131 % for tag_type, tag in tags:
132 132 ${h.style_metatag(tag_type, tag)|n}
133 133 % endfor
134 134 % endif
135 135 ${description}
136 136 </div>
137 137
138 138 </%def>
139 139
140 140 <%def name="last_change(last_change)">
141 141 ${h.age_component(last_change, time_is_local=True)}
142 142 </%def>
143 143
144 144 <%def name="revision(repo_name, rev, commit_id, author, last_msg, commit_date)">
145 145 <div>
146 146 %if rev >= 0:
147 147 <code><a class="tooltip-hovercard" data-hovercard-alt=${h.tooltip(last_msg)} data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)}" href="${h.route_path('repo_commit',repo_name=repo_name,commit_id=commit_id)}">${'r{}:{}'.format(rev,h.short_id(commit_id))}</a></code>
148 148 %else:
149 149 ${_('No commits yet')}
150 150 %endif
151 151 </div>
152 152 </%def>
153 153
154 154 <%def name="rss(name)">
155 155 %if c.rhodecode_user.username != h.DEFAULT_USER:
156 156 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
157 157 %else:
158 158 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
159 159 %endif
160 160 </%def>
161 161
162 162 <%def name="atom(name)">
163 163 %if c.rhodecode_user.username != h.DEFAULT_USER:
164 164 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
165 165 %else:
166 166 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
167 167 %endif
168 168 </%def>
169 169
170 170 <%def name="repo_actions(repo_name, super_user=True)">
171 171 <div>
172 172 <div class="grid_edit">
173 173 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
174 174 Edit
175 175 </a>
176 176 </div>
177 177 <div class="grid_delete">
178 178 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), request=request)}
179 179 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
180 180 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
181 181 ${h.end_form()}
182 182 </div>
183 183 </div>
184 184 </%def>
185 185
186 186 <%def name="repo_state(repo_state)">
187 187 <div>
188 188 %if repo_state == 'repo_state_pending':
189 189 <div class="tag tag4">${_('Creating')}</div>
190 190 %elif repo_state == 'repo_state_created':
191 191 <div class="tag tag1">${_('Created')}</div>
192 192 %else:
193 193 <div class="tag alert2" title="${h.tooltip(repo_state)}">invalid</div>
194 194 %endif
195 195 </div>
196 196 </%def>
197 197
198 198
199 199 ## REPO GROUP RENDERERS
200 200 <%def name="quick_repo_group_menu(repo_group_name)">
201 201 <i class="icon-more"></i>
202 202 <div class="menu_items_container hidden">
203 203 <ul class="menu_items">
204 204 <li>
205 205 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">${_('Summary')}</a>
206 206 </li>
207 207
208 208 </ul>
209 209 </div>
210 210 </%def>
211 211
212 212 <%def name="repo_group_name(repo_group_name, children_groups=None)">
213 213 <div>
214 214 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
215 215 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
216 216 %if children_groups:
217 217 ${h.literal(' &raquo; '.join(children_groups))}
218 218 %else:
219 219 ${repo_group_name}
220 220 %endif
221 221 </a>
222 222 </div>
223 223 </%def>
224 224
225 225 <%def name="repo_group_desc(description, personal, stylify_metatags)">
226 226
227 227 <%
228 228 if stylify_metatags:
229 229 tags, description = h.extract_metatags(description)
230 230 %>
231 231
232 232 <div class="truncate-wrap">
233 233 % if personal:
234 234 <div class="metatag" tag="personal">${_('personal')}</div>
235 235 % endif
236 236
237 237 % if stylify_metatags:
238 238 % for tag_type, tag in tags:
239 239 ${h.style_metatag(tag_type, tag)|n}
240 240 % endfor
241 241 % endif
242 242 ${description}
243 243 </div>
244 244
245 245 </%def>
246 246
247 247 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
248 248 <div class="grid_edit">
249 249 <a href="${h.route_path('edit_repo_group',repo_group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
250 250 </div>
251 251 <div class="grid_delete">
252 252 ${h.secure_form(h.route_path('edit_repo_group_advanced_delete', repo_group_name=repo_group_name), request=request)}
253 253 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
254 254 onclick="return confirm('"+_ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
255 255 ${h.end_form()}
256 256 </div>
257 257 </%def>
258 258
259 259
260 260 <%def name="user_actions(user_id, username)">
261 261 <div class="grid_edit">
262 262 <a href="${h.route_path('user_edit',user_id=user_id)}" title="${_('Edit')}">
263 263 ${_('Edit')}
264 264 </a>
265 265 </div>
266 266 <div class="grid_delete">
267 267 ${h.secure_form(h.route_path('user_delete', user_id=user_id), request=request)}
268 268 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
269 269 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
270 270 ${h.end_form()}
271 271 </div>
272 272 </%def>
273 273
274 274 <%def name="user_group_actions(user_group_id, user_group_name)">
275 275 <div class="grid_edit">
276 276 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
277 277 </div>
278 278 <div class="grid_delete">
279 279 ${h.secure_form(h.route_path('user_groups_delete', user_group_id=user_group_id), request=request)}
280 280 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
281 281 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
282 282 ${h.end_form()}
283 283 </div>
284 284 </%def>
285 285
286 286
287 287 <%def name="user_name(user_id, username)">
288 288 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.route_path('user_edit', user_id=user_id))}
289 289 </%def>
290 290
291 291 <%def name="user_profile(username)">
292 292 ${base.gravatar_with_user(username, 16, tooltip=True)}
293 293 </%def>
294 294
295 295 <%def name="user_group_name(user_group_name)">
296 296 <div>
297 297 <i class="icon-user-group" title="${_('User group')}"></i>
298 298 ${h.link_to_group(user_group_name)}
299 299 </div>
300 300 </%def>
301 301
302 302
303 303 ## GISTS
304 304
305 305 <%def name="gist_gravatar(full_contact)">
306 306 <div class="gist_gravatar">
307 307 ${base.gravatar(full_contact, 30)}
308 308 </div>
309 309 </%def>
310 310
311 311 <%def name="gist_access_id(gist_access_id, full_contact)">
312 312 <div>
313 313 <b>
314 314 <a href="${h.route_path('gist_show', gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
315 315 </b>
316 316 </div>
317 317 </%def>
318 318
319 319 <%def name="gist_author(full_contact, created_on, expires)">
320 320 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
321 321 </%def>
322 322
323 323
324 324 <%def name="gist_created(created_on)">
325 325 <div class="created">
326 326 ${h.age_component(created_on, time_is_local=True)}
327 327 </div>
328 328 </%def>
329 329
330 330 <%def name="gist_expires(expires)">
331 331 <div class="created">
332 332 %if expires == -1:
333 333 ${_('never')}
334 334 %else:
335 335 ${h.age_component(h.time_to_utcdatetime(expires))}
336 336 %endif
337 337 </div>
338 338 </%def>
339 339
340 340 <%def name="gist_type(gist_type)">
341 341 %if gist_type != 'public':
342 342 <div class="tag">${_('Private')}</div>
343 343 %endif
344 344 </%def>
345 345
346 346 <%def name="gist_description(gist_description)">
347 347 ${gist_description}
348 348 </%def>
349 349
350 350
351 351 ## PULL REQUESTS GRID RENDERERS
352 352
353 353 <%def name="pullrequest_target_repo(repo_name)">
354 354 <div class="truncate">
355 355 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
356 356 </div>
357 357 </%def>
358 358
359 359 <%def name="pullrequest_status(status)">
360 360 <i class="icon-circle review-status-${status}"></i>
361 361 </%def>
362 362
363 363 <%def name="pullrequest_title(title, description)">
364 364 ${title}
365 365 </%def>
366 366
367 367 <%def name="pullrequest_comments(comments_nr)">
368 368 <i class="icon-comment"></i> ${comments_nr}
369 369 </%def>
370 370
371 371 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
372 372 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
373 373 % if short:
374 #${pull_request_id}
374 !${pull_request_id}
375 375 % else:
376 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
376 ${_('Pull request !{}').format(pull_request_id)}
377 377 % endif
378 378 </a>
379 379 </%def>
380 380
381 381 <%def name="pullrequest_updated_on(updated_on)">
382 382 ${h.age_component(h.time_to_utcdatetime(updated_on))}
383 383 </%def>
384 384
385 385 <%def name="pullrequest_author(full_contact)">
386 386 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
387 387 </%def>
388 388
389 389
390 390 ## ARTIFACT RENDERERS
391 391 <%def name="repo_artifact_name(repo_name, file_uid, artifact_display_name)">
392 392 <a href="${h.route_path('repo_artifacts_get', repo_name=repo_name, uid=file_uid)}">
393 393 ${artifact_display_name or '_EMPTY_NAME_'}
394 394 </a>
395 395 </%def>
396 396
397 397 <%def name="repo_artifact_uid(repo_name, file_uid)">
398 398 <code>${h.shorter(file_uid, size=24, prefix=True)}</code>
399 399 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${h.route_url('repo_artifacts_get', repo_name=repo_name, uid=file_uid)}" title="${_('Copy the full url')}"></i>
400 400 </%def>
401 401
402 402 <%def name="repo_artifact_sha256(artifact_sha256)">
403 403 <div class="code">${h.shorter(artifact_sha256, 12)}<i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${artifact_sha256}" title="${_('Copy the sha256 ({})').format(artifact_sha256)}"></i></div>
404 404 </%def>
405 405
406 406 <%def name="repo_artifact_actions(repo_name, file_store_id, file_uid)">
407 407 ## <div class="grid_edit">
408 408 ## <a href="#Edit" title="${_('Edit')}">${_('Edit')}</a>
409 409 ## </div>
410 410 <div class="grid_edit">
411 411 <a href="${h.route_path('repo_artifacts_info', repo_name=repo_name, uid=file_store_id)}" title="${_('Info')}">${_('Info')}</a>
412 412 </div>
413 413 % if h.HasRepoPermissionAny('repository.admin')(c.repo_name):
414 414 <div class="grid_delete">
415 415 ${h.secure_form(h.route_path('repo_artifacts_delete', repo_name=repo_name, uid=file_store_id), request=request)}
416 416 ${h.submit('remove_',_('Delete'),id="remove_artifact_%s" % file_store_id, class_="btn btn-link btn-danger",
417 417 onclick="return confirm('"+_('Confirm to delete this artifact: %s') % file_uid+"');")}
418 418 ${h.end_form()}
419 419 </div>
420 420 % endif
421 421 </%def>
422 422
423 423 <%def name="markup_form(form_id, form_text='', help_text=None)">
424 424
425 425 <div class="markup-form">
426 426 <div class="markup-form-area">
427 427 <div class="markup-form-area-header">
428 428 <ul class="nav-links clearfix">
429 429 <li class="active">
430 430 <a href="#edit-text" tabindex="-1" id="edit-btn_${form_id}">${_('Write')}</a>
431 431 </li>
432 432 <li class="">
433 433 <a href="#preview-text" tabindex="-1" id="preview-btn_${form_id}">${_('Preview')}</a>
434 434 </li>
435 435 </ul>
436 436 </div>
437 437
438 438 <div class="markup-form-area-write" style="display: block;">
439 439 <div id="edit-container_${form_id}">
440 440 <textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
441 441 </div>
442 442 <div id="preview-container_${form_id}" class="clearfix" style="display: none;">
443 443 <div id="preview-box_${form_id}" class="preview-box"></div>
444 444 </div>
445 445 </div>
446 446
447 447 <div class="markup-form-area-footer">
448 448 <div class="toolbar">
449 449 <div class="toolbar-text">
450 450 ${(_('Parsed using %s syntax') % (
451 451 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
452 452 )
453 453 )|n}
454 454 </div>
455 455 </div>
456 456 </div>
457 457 </div>
458 458
459 459 <div class="markup-form-footer">
460 460 % if help_text:
461 461 <span class="help-block">${help_text}</span>
462 462 % endif
463 463 </div>
464 464 </div>
465 465 <script type="text/javascript">
466 466 new MarkupForm('${form_id}');
467 467 </script>
468 468
469 469 </%def>
@@ -1,804 +1,804 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4
5 5 <%def name="title()">
6 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
7 7 %if c.rhodecode_name:
8 8 &middot; ${h.branding(c.rhodecode_name)}
9 9 %endif
10 10 </%def>
11 11
12 12 <%def name="breadcrumbs_links()">
13 13 <span id="pr-title">
14 14 ${c.pull_request.title}
15 15 %if c.pull_request.is_closed():
16 16 (${_('Closed')})
17 17 %endif
18 18 </span>
19 19 <div id="pr-title-edit" class="input" style="display: none;">
20 20 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
21 21 </div>
22 22 </%def>
23 23
24 24 <%def name="menu_bar_nav()">
25 25 ${self.menu_items(active='repositories')}
26 26 </%def>
27 27
28 28 <%def name="menu_bar_subnav()">
29 29 ${self.repo_menu(active='showpullrequest')}
30 30 </%def>
31 31
32 32 <%def name="main()">
33 33
34 34 <script type="text/javascript">
35 35 // TODO: marcink switch this to pyroutes
36 36 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
37 37 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
38 38 </script>
39 39 <div class="box">
40 40
41 41 ${self.breadcrumbs()}
42 42
43 43 <div class="box pr-summary">
44 44
45 45 <div class="summary-details block-left">
46 46 <% summary = lambda n:{False:'summary-short'}.get(n) %>
47 47 <div class="pr-details-title">
48 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
48 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 49 %if c.allowed_to_update:
50 50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 51 % if c.allowed_to_delete:
52 52 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
53 53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
54 54 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
55 55 ${h.end_form()}
56 56 % else:
57 57 ${_('Delete')}
58 58 % endif
59 59 </div>
60 60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
61 61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
62 62 %endif
63 63 </div>
64 64
65 65 <div id="summary" class="fields pr-details-content">
66 66 <div class="field">
67 67 <div class="label-summary">
68 68 <label>${_('Source')}:</label>
69 69 </div>
70 70 <div class="input">
71 71 <div class="pr-origininfo">
72 72 ## branch link is only valid if it is a branch
73 73 <span class="tag">
74 74 %if c.pull_request.source_ref_parts.type == 'branch':
75 75 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
76 76 %else:
77 77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
78 78 %endif
79 79 </span>
80 80 <span class="clone-url">
81 81 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
82 82 </span>
83 83 <br/>
84 84 % if c.ancestor_commit:
85 85 ${_('Common ancestor')}:
86 86 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
87 87 % endif
88 88 </div>
89 89 %if h.is_hg(c.pull_request.source_repo):
90 90 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
91 91 %elif h.is_git(c.pull_request.source_repo):
92 92 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
93 93 %endif
94 94
95 95 <div class="">
96 96 <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
97 97 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
98 98 </div>
99 99
100 100 </div>
101 101 </div>
102 102 <div class="field">
103 103 <div class="label-summary">
104 104 <label>${_('Target')}:</label>
105 105 </div>
106 106 <div class="input">
107 107 <div class="pr-targetinfo">
108 108 ## branch link is only valid if it is a branch
109 109 <span class="tag">
110 110 %if c.pull_request.target_ref_parts.type == 'branch':
111 111 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
112 112 %else:
113 113 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
114 114 %endif
115 115 </span>
116 116 <span class="clone-url">
117 117 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
118 118 </span>
119 119 </div>
120 120 </div>
121 121 </div>
122 122
123 123 ## Link to the shadow repository.
124 124 <div class="field">
125 125 <div class="label-summary">
126 126 <label>${_('Merge')}:</label>
127 127 </div>
128 128 <div class="input">
129 129 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
130 130 %if h.is_hg(c.pull_request.target_repo):
131 131 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
132 132 %elif h.is_git(c.pull_request.target_repo):
133 133 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
134 134 %endif
135 135 <div class="">
136 136 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
137 137 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
138 138 </div>
139 139 % else:
140 140 <div class="">
141 141 ${_('Shadow repository data not available')}.
142 142 </div>
143 143 % endif
144 144 </div>
145 145 </div>
146 146
147 147 <div class="field">
148 148 <div class="label-summary">
149 149 <label>${_('Review')}:</label>
150 150 </div>
151 151 <div class="input">
152 152 %if c.pull_request_review_status:
153 153 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
154 154 <span class="changeset-status-lbl tooltip">
155 155 %if c.pull_request.is_closed():
156 156 ${_('Closed')},
157 157 %endif
158 158 ${h.commit_status_lbl(c.pull_request_review_status)}
159 159 </span>
160 160 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
161 161 %endif
162 162 </div>
163 163 </div>
164 164 <div class="field">
165 165 <div class="pr-description-label label-summary" title="${_('Rendered using {} renderer').format(c.renderer)}">
166 166 <label>${_('Description')}:</label>
167 167 </div>
168 168 <div id="pr-desc" class="input">
169 169 <div class="pr-description">${h.render(c.pull_request.description, renderer=c.renderer)}</div>
170 170 </div>
171 171 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
172 172 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
173 173 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
174 174 </div>
175 175 </div>
176 176
177 177 <div class="field">
178 178 <div class="label-summary">
179 179 <label>${_('Versions')}:</label>
180 180 </div>
181 181
182 182 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
183 183 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
184 184
185 185 <div class="pr-versions">
186 186 % if c.show_version_changes:
187 187 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
188 188 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
189 189 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
190 190 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
191 191 data-toggle-off="${_('Hide all versions of this pull request')}">
192 192 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
193 193 </a>
194 194 <table>
195 195 ## SHOW ALL VERSIONS OF PR
196 196 <% ver_pr = None %>
197 197
198 198 % for data in reversed(list(enumerate(c.versions, 1))):
199 199 <% ver_pos = data[0] %>
200 200 <% ver = data[1] %>
201 201 <% ver_pr = ver.pull_request_version_id %>
202 202 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
203 203
204 204 <tr class="version-pr" style="display: ${display_row}">
205 205 <td>
206 206 <code>
207 207 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
208 208 </code>
209 209 </td>
210 210 <td>
211 211 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
212 212 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
213 213 </td>
214 214 <td>
215 215 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
216 216 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
217 217 </div>
218 218 </td>
219 219 <td>
220 220 % if c.at_version_num != ver_pr:
221 221 <i class="icon-comment"></i>
222 222 <code class="tooltip" title="${_('Comment from pull request version v{0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
223 223 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
224 224 </code>
225 225 % endif
226 226 </td>
227 227 <td>
228 228 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
229 229 </td>
230 230 <td>
231 231 ${h.age_component(ver.updated_on, time_is_local=True)}
232 232 </td>
233 233 </tr>
234 234 % endfor
235 235
236 236 <tr>
237 237 <td colspan="6">
238 238 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
239 239 data-label-text-locked="${_('select versions to show changes')}"
240 240 data-label-text-diff="${_('show changes between versions')}"
241 241 data-label-text-show="${_('show pull request for this version')}"
242 242 >
243 243 ${_('select versions to show changes')}
244 244 </button>
245 245 </td>
246 246 </tr>
247 247 </table>
248 248 % else:
249 249 <div class="input">
250 250 ${_('Pull request versions not available')}.
251 251 </div>
252 252 % endif
253 253 </div>
254 254 </div>
255 255
256 256 <div id="pr-save" class="field" style="display: none;">
257 257 <div class="label-summary"></div>
258 258 <div class="input">
259 259 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
260 260 </div>
261 261 </div>
262 262 </div>
263 263 </div>
264 264 <div>
265 265 ## AUTHOR
266 266 <div class="reviewers-title block-right">
267 267 <div class="pr-details-title">
268 268 ${_('Author of this pull request')}
269 269 </div>
270 270 </div>
271 271 <div class="block-right pr-details-content reviewers">
272 272 <ul class="group_members">
273 273 <li>
274 274 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
275 275 </li>
276 276 </ul>
277 277 </div>
278 278
279 279 ## REVIEW RULES
280 280 <div id="review_rules" style="display: none" class="reviewers-title block-right">
281 281 <div class="pr-details-title">
282 282 ${_('Reviewer rules')}
283 283 %if c.allowed_to_update:
284 284 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
285 285 %endif
286 286 </div>
287 287 <div class="pr-reviewer-rules">
288 288 ## review rules will be appended here, by default reviewers logic
289 289 </div>
290 290 <input id="review_data" type="hidden" name="review_data" value="">
291 291 </div>
292 292
293 293 ## REVIEWERS
294 294 <div class="reviewers-title block-right">
295 295 <div class="pr-details-title">
296 296 ${_('Pull request reviewers')}
297 297 %if c.allowed_to_update:
298 298 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
299 299 %endif
300 300 </div>
301 301 </div>
302 302 <div id="reviewers" class="block-right pr-details-content reviewers">
303 303
304 304 ## members redering block
305 305 <input type="hidden" name="__start__" value="review_members:sequence">
306 306 <ul id="review_members" class="group_members">
307 307
308 308 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
309 309 <script>
310 310 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
311 311 var status = "${(status[0][1].status if status else 'not_reviewed')}";
312 312 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
313 313 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
314 314
315 315 var entry = renderTemplate('reviewMemberEntry', {
316 316 'member': member,
317 317 'mandatory': member.mandatory,
318 318 'reasons': member.reasons,
319 319 'allowed_to_update': allowed_to_update,
320 320 'review_status': status,
321 321 'review_status_label': status_lbl,
322 322 'user_group': member.user_group,
323 323 'create': false
324 324 });
325 325 $('#review_members').append(entry)
326 326 </script>
327 327
328 328 % endfor
329 329
330 330 </ul>
331 331
332 332 <input type="hidden" name="__end__" value="review_members:sequence">
333 333 ## end members redering block
334 334
335 335 %if not c.pull_request.is_closed():
336 336 <div id="add_reviewer" class="ac" style="display: none;">
337 337 %if c.allowed_to_update:
338 338 % if not c.forbid_adding_reviewers:
339 339 <div id="add_reviewer_input" class="reviewer_ac">
340 340 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
341 341 <div id="reviewers_container"></div>
342 342 </div>
343 343 % endif
344 344 <div class="pull-right">
345 345 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
346 346 </div>
347 347 %endif
348 348 </div>
349 349 %endif
350 350 </div>
351 351 </div>
352 352 </div>
353 353 <div class="box">
354 354 ##DIFF
355 355 <div class="table" >
356 356 <div id="changeset_compare_view_content">
357 357 ##CS
358 358 % if c.missing_requirements:
359 359 <div class="box">
360 360 <div class="alert alert-warning">
361 361 <div>
362 362 <strong>${_('Missing requirements:')}</strong>
363 363 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
364 364 </div>
365 365 </div>
366 366 </div>
367 367 % elif c.missing_commits:
368 368 <div class="box">
369 369 <div class="alert alert-warning">
370 370 <div>
371 371 <strong>${_('Missing commits')}:</strong>
372 372 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
373 373 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
374 374 ${_('Consider doing a {force_refresh_url} in case you think this is an error.').format(force_refresh_url=h.link_to('force refresh', h.current_route_path(request, force_refresh='1')))|n}
375 375 </div>
376 376 </div>
377 377 </div>
378 378 % endif
379 379
380 380 <div class="compare_view_commits_title">
381 381 % if not c.compare_mode:
382 382
383 383 % if c.at_version_pos:
384 384 <h4>
385 385 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
386 386 </h4>
387 387 % endif
388 388
389 389 <div class="pull-left">
390 390 <div class="btn-group">
391 391 <a
392 392 class="btn"
393 393 href="#"
394 394 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
395 395 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
396 396 </a>
397 397 <a
398 398 class="btn"
399 399 href="#"
400 400 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
401 401 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
402 402 </a>
403 403 </div>
404 404 </div>
405 405
406 406 <div class="pull-right">
407 407 % if c.allowed_to_update and not c.pull_request.is_closed():
408 408 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
409 409 % else:
410 410 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
411 411 % endif
412 412
413 413 </div>
414 414 % endif
415 415 </div>
416 416
417 417 % if not c.missing_commits:
418 418 % if c.compare_mode:
419 419 % if c.at_version:
420 420 <h4>
421 421 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
422 422 </h4>
423 423
424 424 <div class="subtitle-compare">
425 425 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
426 426 </div>
427 427
428 428 <div class="container">
429 429 <table class="rctable compare_view_commits">
430 430 <tr>
431 431 <th></th>
432 432 <th>${_('Time')}</th>
433 433 <th>${_('Author')}</th>
434 434 <th>${_('Commit')}</th>
435 435 <th></th>
436 436 <th>${_('Description')}</th>
437 437 </tr>
438 438
439 439 % for c_type, commit in c.commit_changes:
440 440 % if c_type in ['a', 'r']:
441 441 <%
442 442 if c_type == 'a':
443 443 cc_title = _('Commit added in displayed changes')
444 444 elif c_type == 'r':
445 445 cc_title = _('Commit removed in displayed changes')
446 446 else:
447 447 cc_title = ''
448 448 %>
449 449 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
450 450 <td>
451 451 <div class="commit-change-indicator color-${c_type}-border">
452 452 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
453 453 ${c_type.upper()}
454 454 </div>
455 455 </div>
456 456 </td>
457 457 <td class="td-time">
458 458 ${h.age_component(commit.date)}
459 459 </td>
460 460 <td class="td-user">
461 461 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
462 462 </td>
463 463 <td class="td-hash">
464 464 <code>
465 465 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
466 466 r${commit.idx}:${h.short_id(commit.raw_id)}
467 467 </a>
468 468 ${h.hidden('revisions', commit.raw_id)}
469 469 </code>
470 470 </td>
471 471 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
472 472 <i class="icon-expand-linked"></i>
473 473 </td>
474 474 <td class="mid td-description">
475 475 <div class="log-container truncate-wrap">
476 476 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
477 477 </div>
478 478 </td>
479 479 </tr>
480 480 % endif
481 481 % endfor
482 482 </table>
483 483 </div>
484 484
485 485 % endif
486 486
487 487 % else:
488 488 <%include file="/compare/compare_commits.mako" />
489 489 % endif
490 490
491 491 <div class="cs_files">
492 492 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
493 493 % if c.at_version:
494 494 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
495 495 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
496 496 % else:
497 497 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
498 498 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
499 499 % endif
500 500
501 501 <%
502 502 pr_menu_data = {
503 503 'outdated_comm_count_ver': outdated_comm_count_ver
504 504 }
505 505 %>
506 506
507 507 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
508 508
509 509 % if c.range_diff_on:
510 510 % for commit in c.commit_ranges:
511 511 ${cbdiffs.render_diffset(
512 512 c.changes[commit.raw_id],
513 513 commit=commit, use_comments=True,
514 514 collapse_when_files_over=5,
515 515 disable_new_comments=True,
516 516 deleted_files_comments=c.deleted_files_comments,
517 517 inline_comments=c.inline_comments,
518 518 pull_request_menu=pr_menu_data)}
519 519 % endfor
520 520 % else:
521 521 ${cbdiffs.render_diffset(
522 522 c.diffset, use_comments=True,
523 523 collapse_when_files_over=30,
524 524 disable_new_comments=not c.allowed_to_comment,
525 525 deleted_files_comments=c.deleted_files_comments,
526 526 inline_comments=c.inline_comments,
527 527 pull_request_menu=pr_menu_data)}
528 528 % endif
529 529
530 530 </div>
531 531 % else:
532 532 ## skipping commits we need to clear the view for missing commits
533 533 <div style="clear:both;"></div>
534 534 % endif
535 535
536 536 </div>
537 537 </div>
538 538
539 539 ## template for inline comment form
540 540 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
541 541
542 542 ## comments heading with count
543 543 <div class="comments-heading">
544 544 <i class="icon-comment"></i>
545 545 ${_('Comments')} ${len(c.comments)}
546 546 </div>
547 547
548 548 ## render general comments
549 549 <div id="comment-tr-show">
550 550 % if general_outdated_comm_count_ver:
551 551 <div class="info-box">
552 552 % if general_outdated_comm_count_ver == 1:
553 553 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
554 554 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
555 555 % else:
556 556 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
557 557 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
558 558 % endif
559 559 </div>
560 560 % endif
561 561 </div>
562 562
563 563 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
564 564
565 565 % if not c.pull_request.is_closed():
566 566 ## merge status, and merge action
567 567 <div class="pull-request-merge">
568 568 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
569 569 </div>
570 570
571 571 ## main comment form and it status
572 572 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
573 573 pull_request_id=c.pull_request.pull_request_id),
574 574 c.pull_request_review_status,
575 575 is_pull_request=True, change_status=c.allowed_to_change_status)}
576 576 %endif
577 577
578 578 <script type="text/javascript">
579 579 if (location.hash) {
580 580 var result = splitDelimitedHash(location.hash);
581 581 var line = $('html').find(result.loc);
582 582 // show hidden comments if we use location.hash
583 583 if (line.hasClass('comment-general')) {
584 584 $(line).show();
585 585 } else if (line.hasClass('comment-inline')) {
586 586 $(line).show();
587 587 var $cb = $(line).closest('.cb');
588 588 $cb.removeClass('cb-collapsed')
589 589 }
590 590 if (line.length > 0){
591 591 offsetScroll(line, 70);
592 592 }
593 593 }
594 594
595 595 versionController = new VersionController();
596 596 versionController.init();
597 597
598 598 reviewersController = new ReviewersController();
599 599 commitsController = new CommitsController();
600 600
601 601 $(function(){
602 602
603 603 // custom code mirror
604 604 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
605 605
606 606 var PRDetails = {
607 607 editButton: $('#open_edit_pullrequest'),
608 608 closeButton: $('#close_edit_pullrequest'),
609 609 deleteButton: $('#delete_pullrequest'),
610 610 viewFields: $('#pr-desc, #pr-title'),
611 611 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
612 612
613 613 init: function() {
614 614 var that = this;
615 615 this.editButton.on('click', function(e) { that.edit(); });
616 616 this.closeButton.on('click', function(e) { that.view(); });
617 617 },
618 618
619 619 edit: function(event) {
620 620 this.viewFields.hide();
621 621 this.editButton.hide();
622 622 this.deleteButton.hide();
623 623 this.closeButton.show();
624 624 this.editFields.show();
625 625 codeMirrorInstance.refresh();
626 626 },
627 627
628 628 view: function(event) {
629 629 this.editButton.show();
630 630 this.deleteButton.show();
631 631 this.editFields.hide();
632 632 this.closeButton.hide();
633 633 this.viewFields.show();
634 634 }
635 635 };
636 636
637 637 var ReviewersPanel = {
638 638 editButton: $('#open_edit_reviewers'),
639 639 closeButton: $('#close_edit_reviewers'),
640 640 addButton: $('#add_reviewer'),
641 641 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
642 642
643 643 init: function() {
644 644 var self = this;
645 645 this.editButton.on('click', function(e) { self.edit(); });
646 646 this.closeButton.on('click', function(e) { self.close(); });
647 647 },
648 648
649 649 edit: function(event) {
650 650 this.editButton.hide();
651 651 this.closeButton.show();
652 652 this.addButton.show();
653 653 this.removeButtons.css('visibility', 'visible');
654 654 // review rules
655 655 reviewersController.loadReviewRules(
656 656 ${c.pull_request.reviewer_data_json | n});
657 657 },
658 658
659 659 close: function(event) {
660 660 this.editButton.show();
661 661 this.closeButton.hide();
662 662 this.addButton.hide();
663 663 this.removeButtons.css('visibility', 'hidden');
664 664 // hide review rules
665 665 reviewersController.hideReviewRules()
666 666 }
667 667 };
668 668
669 669 PRDetails.init();
670 670 ReviewersPanel.init();
671 671
672 672 showOutdated = function(self){
673 673 $('.comment-inline.comment-outdated').show();
674 674 $('.filediff-outdated').show();
675 675 $('.showOutdatedComments').hide();
676 676 $('.hideOutdatedComments').show();
677 677 };
678 678
679 679 hideOutdated = function(self){
680 680 $('.comment-inline.comment-outdated').hide();
681 681 $('.filediff-outdated').hide();
682 682 $('.hideOutdatedComments').hide();
683 683 $('.showOutdatedComments').show();
684 684 };
685 685
686 686 refreshMergeChecks = function(){
687 687 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
688 688 $('.pull-request-merge').css('opacity', 0.3);
689 689 $('.action-buttons-extra').css('opacity', 0.3);
690 690
691 691 $('.pull-request-merge').load(
692 692 loadUrl, function() {
693 693 $('.pull-request-merge').css('opacity', 1);
694 694
695 695 $('.action-buttons-extra').css('opacity', 1);
696 696 }
697 697 );
698 698 };
699 699
700 700 closePullRequest = function (status) {
701 701 // inject closing flag
702 702 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
703 703 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
704 704 $(generalCommentForm.submitForm).submit();
705 705 };
706 706
707 707 $('#show-outdated-comments').on('click', function(e){
708 708 var button = $(this);
709 709 var outdated = $('.comment-outdated');
710 710
711 711 if (button.html() === "(Show)") {
712 712 button.html("(Hide)");
713 713 outdated.show();
714 714 } else {
715 715 button.html("(Show)");
716 716 outdated.hide();
717 717 }
718 718 });
719 719
720 720 $('.show-inline-comments').on('change', function(e){
721 721 var show = 'none';
722 722 var target = e.currentTarget;
723 723 if(target.checked){
724 724 show = ''
725 725 }
726 726 var boxid = $(target).attr('id_for');
727 727 var comments = $('#{0} .inline-comments'.format(boxid));
728 728 var fn_display = function(idx){
729 729 $(this).css('display', show);
730 730 };
731 731 $(comments).each(fn_display);
732 732 var btns = $('#{0} .inline-comments-button'.format(boxid));
733 733 $(btns).each(fn_display);
734 734 });
735 735
736 736 $('#merge_pull_request_form').submit(function() {
737 737 if (!$('#merge_pull_request').attr('disabled')) {
738 738 $('#merge_pull_request').attr('disabled', 'disabled');
739 739 }
740 740 return true;
741 741 });
742 742
743 743 $('#edit_pull_request').on('click', function(e){
744 744 var title = $('#pr-title-input').val();
745 745 var description = codeMirrorInstance.getValue();
746 746 var renderer = $('#pr-renderer-input').val();
747 747 editPullRequest(
748 748 "${c.repo_name}", "${c.pull_request.pull_request_id}",
749 749 title, description, renderer);
750 750 });
751 751
752 752 $('#update_pull_request').on('click', function(e){
753 753 $(this).attr('disabled', 'disabled');
754 754 $(this).addClass('disabled');
755 755 $(this).html(_gettext('Saving...'));
756 756 reviewersController.updateReviewers(
757 757 "${c.repo_name}", "${c.pull_request.pull_request_id}");
758 758 });
759 759
760 760 $('#update_commits').on('click', function(e){
761 761 var isDisabled = !$(e.currentTarget).attr('disabled');
762 762 $(e.currentTarget).attr('disabled', 'disabled');
763 763 $(e.currentTarget).addClass('disabled');
764 764 $(e.currentTarget).removeClass('btn-primary');
765 765 $(e.currentTarget).text(_gettext('Updating...'));
766 766 if(isDisabled){
767 767 updateCommits(
768 768 "${c.repo_name}", "${c.pull_request.pull_request_id}");
769 769 }
770 770 });
771 771 // fixing issue with caches on firefox
772 772 $('#update_commits').removeAttr("disabled");
773 773
774 774 $('.show-inline-comments').on('click', function(e){
775 775 var boxid = $(this).attr('data-comment-id');
776 776 var button = $(this);
777 777
778 778 if(button.hasClass("comments-visible")) {
779 779 $('#{0} .inline-comments'.format(boxid)).each(function(index){
780 780 $(this).hide();
781 781 });
782 782 button.removeClass("comments-visible");
783 783 } else {
784 784 $('#{0} .inline-comments'.format(boxid)).each(function(index){
785 785 $(this).show();
786 786 });
787 787 button.addClass("comments-visible");
788 788 }
789 789 });
790 790
791 791 // register submit callback on commentForm form to track TODOs
792 792 window.commentFormGlobalSubmitSuccessCallback = function(){
793 793 refreshMergeChecks();
794 794 };
795 795
796 796 ReviewerAutoComplete('#user');
797 797
798 798 })
799 799 </script>
800 800
801 801 </div>
802 802 </div>
803 803
804 804 </%def>
@@ -1,121 +1,121 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Requests') % c.repo_name}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()"></%def>
11 11
12 12 <%def name="menu_bar_nav()">
13 13 ${self.menu_items(active='repositories')}
14 14 </%def>
15 15
16 16
17 17 <%def name="menu_bar_subnav()">
18 18 ${self.repo_menu(active='showpullrequest')}
19 19 </%def>
20 20
21 21
22 22 <%def name="main()">
23 23
24 24 <div class="box">
25 25 <div class="title">
26 26 <ul class="button-links">
27 27 <li class="btn ${('active' if c.active=='open' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
28 28 <li class="btn ${('active' if c.active=='my' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
29 29 <li class="btn ${('active' if c.active=='awaiting' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
30 30 <li class="btn ${('active' if c.active=='awaiting_my' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
31 31 <li class="btn ${('active' if c.active=='closed' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
32 32 <li class="btn ${('active' if c.active=='source' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
33 33 </ul>
34 34
35 35 <ul class="links">
36 36 % if c.rhodecode_user.username != h.DEFAULT_USER:
37 37 <li>
38 38 <span>
39 39 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
40 40 ${_('Open new Pull Request')}
41 41 </a>
42 42 </span>
43 43 </li>
44 44 % endif
45 45 </ul>
46 46
47 47 </div>
48 48
49 49 <div class="main-content-full-width">
50 50 <table id="pull_request_list_table" class="display"></table>
51 51 </div>
52 52
53 53 </div>
54 54
55 55 <script type="text/javascript">
56 56 $(document).ready(function() {
57 57
58 58 var $pullRequestListTable = $('#pull_request_list_table');
59 59
60 60 // object list
61 61 $pullRequestListTable.DataTable({
62 62 processing: true,
63 63 serverSide: true,
64 64 ajax: {
65 65 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
66 66 "data": function (d) {
67 67 d.source = "${c.source}";
68 68 d.closed = "${c.closed}";
69 69 d.my = "${c.my}";
70 70 d.awaiting_review = "${c.awaiting_review}";
71 71 d.awaiting_my_review = "${c.awaiting_my_review}";
72 72 }
73 73 },
74 74 dom: 'rtp',
75 75 pageLength: ${c.visual.dashboard_items},
76 76 order: [[ 1, "desc" ]],
77 77 columns: [
78 78 { data: {"_": "status",
79 79 "sort": "status"}, title: "", className: "td-status", orderable: false},
80 80 { data: {"_": "name",
81 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
81 "sort": "name_raw"}, title: "${_('Id')}", className: "td-componentname", "type": "num" },
82 { data: {"_": "title",
83 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
82 84 { data: {"_": "author",
83 85 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
84 { data: {"_": "title",
85 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
86 86 { data: {"_": "comments",
87 87 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
88 88 { data: {"_": "updated_on",
89 89 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
90 90 ],
91 91 language: {
92 92 paginate: DEFAULT_GRID_PAGINATION,
93 93 sProcessing: _gettext('loading...'),
94 94 emptyTable: _gettext("No pull requests available yet.")
95 95 },
96 96 "drawCallback": function( settings, json ) {
97 97 timeagoActivate();
98 98 tooltipActivate();
99 99 },
100 100 "createdRow": function ( row, data, index ) {
101 101 if (data['closed']) {
102 102 $(row).addClass('closed');
103 103 }
104 104 if (data['state'] !== 'created') {
105 105 $(row).addClass('state-' + data['state']);
106 106 }
107 107 }
108 108 });
109 109
110 110 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
111 111 $pullRequestListTable.css('opacity', 1);
112 112 });
113 113
114 114 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
115 115 $pullRequestListTable.css('opacity', 0.3);
116 116 });
117 117
118 118 });
119 119
120 120 </script>
121 121 </%def>
@@ -1,964 +1,964 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23 import textwrap
24 24
25 25 import rhodecode
26 26 from rhodecode.lib.utils2 import safe_unicode
27 27 from rhodecode.lib.vcs.backends import get_backend
28 28 from rhodecode.lib.vcs.backends.base import (
29 29 MergeResponse, MergeFailureReason, Reference)
30 30 from rhodecode.lib.vcs.exceptions import RepositoryError
31 31 from rhodecode.lib.vcs.nodes import FileNode
32 32 from rhodecode.model.comment import CommentsModel
33 33 from rhodecode.model.db import PullRequest, Session
34 34 from rhodecode.model.pull_request import PullRequestModel
35 35 from rhodecode.model.user import UserModel
36 36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
37 37
38 38
39 39 pytestmark = [
40 40 pytest.mark.backends("git", "hg"),
41 41 ]
42 42
43 43
44 44 @pytest.mark.usefixtures('config_stub')
45 45 class TestPullRequestModel(object):
46 46
47 47 @pytest.fixture()
48 48 def pull_request(self, request, backend, pr_util):
49 49 """
50 50 A pull request combined with multiples patches.
51 51 """
52 52 BackendClass = get_backend(backend.alias)
53 53 merge_resp = MergeResponse(
54 54 False, False, None, MergeFailureReason.UNKNOWN,
55 55 metadata={'exception': 'MockError'})
56 56 self.merge_patcher = mock.patch.object(
57 57 BackendClass, 'merge', return_value=merge_resp)
58 58 self.workspace_remove_patcher = mock.patch.object(
59 59 BackendClass, 'cleanup_merge_workspace')
60 60
61 61 self.workspace_remove_mock = self.workspace_remove_patcher.start()
62 62 self.merge_mock = self.merge_patcher.start()
63 63 self.comment_patcher = mock.patch(
64 64 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
65 65 self.comment_patcher.start()
66 66 self.notification_patcher = mock.patch(
67 67 'rhodecode.model.notification.NotificationModel.create')
68 68 self.notification_patcher.start()
69 69 self.helper_patcher = mock.patch(
70 70 'rhodecode.lib.helpers.route_path')
71 71 self.helper_patcher.start()
72 72
73 73 self.hook_patcher = mock.patch.object(PullRequestModel,
74 74 'trigger_pull_request_hook')
75 75 self.hook_mock = self.hook_patcher.start()
76 76
77 77 self.invalidation_patcher = mock.patch(
78 78 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
79 79 self.invalidation_mock = self.invalidation_patcher.start()
80 80
81 81 self.pull_request = pr_util.create_pull_request(
82 82 mergeable=True, name_suffix=u'Δ…Δ‡')
83 83 self.source_commit = self.pull_request.source_ref_parts.commit_id
84 84 self.target_commit = self.pull_request.target_ref_parts.commit_id
85 85 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
86 86 self.repo_id = self.pull_request.target_repo.repo_id
87 87
88 88 @request.addfinalizer
89 89 def cleanup_pull_request():
90 90 calls = [mock.call(
91 91 self.pull_request, self.pull_request.author, 'create')]
92 92 self.hook_mock.assert_has_calls(calls)
93 93
94 94 self.workspace_remove_patcher.stop()
95 95 self.merge_patcher.stop()
96 96 self.comment_patcher.stop()
97 97 self.notification_patcher.stop()
98 98 self.helper_patcher.stop()
99 99 self.hook_patcher.stop()
100 100 self.invalidation_patcher.stop()
101 101
102 102 return self.pull_request
103 103
104 104 def test_get_all(self, pull_request):
105 105 prs = PullRequestModel().get_all(pull_request.target_repo)
106 106 assert isinstance(prs, list)
107 107 assert len(prs) == 1
108 108
109 109 def test_count_all(self, pull_request):
110 110 pr_count = PullRequestModel().count_all(pull_request.target_repo)
111 111 assert pr_count == 1
112 112
113 113 def test_get_awaiting_review(self, pull_request):
114 114 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
115 115 assert isinstance(prs, list)
116 116 assert len(prs) == 1
117 117
118 118 def test_count_awaiting_review(self, pull_request):
119 119 pr_count = PullRequestModel().count_awaiting_review(
120 120 pull_request.target_repo)
121 121 assert pr_count == 1
122 122
123 123 def test_get_awaiting_my_review(self, pull_request):
124 124 PullRequestModel().update_reviewers(
125 125 pull_request, [(pull_request.author, ['author'], False, [])],
126 126 pull_request.author)
127 127 Session().commit()
128 128
129 129 prs = PullRequestModel().get_awaiting_my_review(
130 130 pull_request.target_repo, user_id=pull_request.author.user_id)
131 131 assert isinstance(prs, list)
132 132 assert len(prs) == 1
133 133
134 134 def test_count_awaiting_my_review(self, pull_request):
135 135 PullRequestModel().update_reviewers(
136 136 pull_request, [(pull_request.author, ['author'], False, [])],
137 137 pull_request.author)
138 138 Session().commit()
139 139
140 140 pr_count = PullRequestModel().count_awaiting_my_review(
141 141 pull_request.target_repo, user_id=pull_request.author.user_id)
142 142 assert pr_count == 1
143 143
144 144 def test_delete_calls_cleanup_merge(self, pull_request):
145 145 repo_id = pull_request.target_repo.repo_id
146 146 PullRequestModel().delete(pull_request, pull_request.author)
147 147 Session().commit()
148 148
149 149 self.workspace_remove_mock.assert_called_once_with(
150 150 repo_id, self.workspace_id)
151 151
152 152 def test_close_calls_cleanup_and_hook(self, pull_request):
153 153 PullRequestModel().close_pull_request(
154 154 pull_request, pull_request.author)
155 155 Session().commit()
156 156
157 157 repo_id = pull_request.target_repo.repo_id
158 158
159 159 self.workspace_remove_mock.assert_called_once_with(
160 160 repo_id, self.workspace_id)
161 161 self.hook_mock.assert_called_with(
162 162 self.pull_request, self.pull_request.author, 'close')
163 163
164 164 def test_merge_status(self, pull_request):
165 165 self.merge_mock.return_value = MergeResponse(
166 166 True, False, None, MergeFailureReason.NONE)
167 167
168 168 assert pull_request._last_merge_source_rev is None
169 169 assert pull_request._last_merge_target_rev is None
170 170 assert pull_request.last_merge_status is None
171 171
172 172 status, msg = PullRequestModel().merge_status(pull_request)
173 173 assert status is True
174 174 assert msg == 'This pull request can be automatically merged.'
175 175 self.merge_mock.assert_called_with(
176 176 self.repo_id, self.workspace_id,
177 177 pull_request.target_ref_parts,
178 178 pull_request.source_repo.scm_instance(),
179 179 pull_request.source_ref_parts, dry_run=True,
180 180 use_rebase=False, close_branch=False)
181 181
182 182 assert pull_request._last_merge_source_rev == self.source_commit
183 183 assert pull_request._last_merge_target_rev == self.target_commit
184 184 assert pull_request.last_merge_status is MergeFailureReason.NONE
185 185
186 186 self.merge_mock.reset_mock()
187 187 status, msg = PullRequestModel().merge_status(pull_request)
188 188 assert status is True
189 189 assert msg == 'This pull request can be automatically merged.'
190 190 assert self.merge_mock.called is False
191 191
192 192 def test_merge_status_known_failure(self, pull_request):
193 193 self.merge_mock.return_value = MergeResponse(
194 194 False, False, None, MergeFailureReason.MERGE_FAILED)
195 195
196 196 assert pull_request._last_merge_source_rev is None
197 197 assert pull_request._last_merge_target_rev is None
198 198 assert pull_request.last_merge_status is None
199 199
200 200 status, msg = PullRequestModel().merge_status(pull_request)
201 201 assert status is False
202 202 assert msg == 'This pull request cannot be merged because of merge conflicts.'
203 203 self.merge_mock.assert_called_with(
204 204 self.repo_id, self.workspace_id,
205 205 pull_request.target_ref_parts,
206 206 pull_request.source_repo.scm_instance(),
207 207 pull_request.source_ref_parts, dry_run=True,
208 208 use_rebase=False, close_branch=False)
209 209
210 210 assert pull_request._last_merge_source_rev == self.source_commit
211 211 assert pull_request._last_merge_target_rev == self.target_commit
212 212 assert (
213 213 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
214 214
215 215 self.merge_mock.reset_mock()
216 216 status, msg = PullRequestModel().merge_status(pull_request)
217 217 assert status is False
218 218 assert msg == 'This pull request cannot be merged because of merge conflicts.'
219 219 assert self.merge_mock.called is False
220 220
221 221 def test_merge_status_unknown_failure(self, pull_request):
222 222 self.merge_mock.return_value = MergeResponse(
223 223 False, False, None, MergeFailureReason.UNKNOWN,
224 224 metadata={'exception': 'MockError'})
225 225
226 226 assert pull_request._last_merge_source_rev is None
227 227 assert pull_request._last_merge_target_rev is None
228 228 assert pull_request.last_merge_status is None
229 229
230 230 status, msg = PullRequestModel().merge_status(pull_request)
231 231 assert status is False
232 232 assert msg == (
233 233 'This pull request cannot be merged because of an unhandled exception. '
234 234 'MockError')
235 235 self.merge_mock.assert_called_with(
236 236 self.repo_id, self.workspace_id,
237 237 pull_request.target_ref_parts,
238 238 pull_request.source_repo.scm_instance(),
239 239 pull_request.source_ref_parts, dry_run=True,
240 240 use_rebase=False, close_branch=False)
241 241
242 242 assert pull_request._last_merge_source_rev is None
243 243 assert pull_request._last_merge_target_rev is None
244 244 assert pull_request.last_merge_status is None
245 245
246 246 self.merge_mock.reset_mock()
247 247 status, msg = PullRequestModel().merge_status(pull_request)
248 248 assert status is False
249 249 assert msg == (
250 250 'This pull request cannot be merged because of an unhandled exception. '
251 251 'MockError')
252 252 assert self.merge_mock.called is True
253 253
254 254 def test_merge_status_when_target_is_locked(self, pull_request):
255 255 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
256 256 status, msg = PullRequestModel().merge_status(pull_request)
257 257 assert status is False
258 258 assert msg == (
259 259 'This pull request cannot be merged because the target repository '
260 260 'is locked by user:1.')
261 261
262 262 def test_merge_status_requirements_check_target(self, pull_request):
263 263
264 264 def has_largefiles(self, repo):
265 265 return repo == pull_request.source_repo
266 266
267 267 patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles)
268 268 with patcher:
269 269 status, msg = PullRequestModel().merge_status(pull_request)
270 270
271 271 assert status is False
272 272 assert msg == 'Target repository large files support is disabled.'
273 273
274 274 def test_merge_status_requirements_check_source(self, pull_request):
275 275
276 276 def has_largefiles(self, repo):
277 277 return repo == pull_request.target_repo
278 278
279 279 patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles)
280 280 with patcher:
281 281 status, msg = PullRequestModel().merge_status(pull_request)
282 282
283 283 assert status is False
284 284 assert msg == 'Source repository large files support is disabled.'
285 285
286 286 def test_merge(self, pull_request, merge_extras):
287 287 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
288 288 merge_ref = Reference(
289 289 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
290 290 self.merge_mock.return_value = MergeResponse(
291 291 True, True, merge_ref, MergeFailureReason.NONE)
292 292
293 293 merge_extras['repository'] = pull_request.target_repo.repo_name
294 294 PullRequestModel().merge_repo(
295 295 pull_request, pull_request.author, extras=merge_extras)
296 296 Session().commit()
297 297
298 298 message = (
299 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
299 u'Merge pull request !{pr_id} from {source_repo} {source_ref_name}'
300 300 u'\n\n {pr_title}'.format(
301 301 pr_id=pull_request.pull_request_id,
302 302 source_repo=safe_unicode(
303 303 pull_request.source_repo.scm_instance().name),
304 304 source_ref_name=pull_request.source_ref_parts.name,
305 305 pr_title=safe_unicode(pull_request.title)
306 306 )
307 307 )
308 308 self.merge_mock.assert_called_with(
309 309 self.repo_id, self.workspace_id,
310 310 pull_request.target_ref_parts,
311 311 pull_request.source_repo.scm_instance(),
312 312 pull_request.source_ref_parts,
313 313 user_name=user.short_contact, user_email=user.email, message=message,
314 314 use_rebase=False, close_branch=False
315 315 )
316 316 self.invalidation_mock.assert_called_once_with(
317 317 pull_request.target_repo.repo_name)
318 318
319 319 self.hook_mock.assert_called_with(
320 320 self.pull_request, self.pull_request.author, 'merge')
321 321
322 322 pull_request = PullRequest.get(pull_request.pull_request_id)
323 323 assert pull_request.merge_rev == '6126b7bfcc82ad2d3deaee22af926b082ce54cc6'
324 324
325 325 def test_merge_with_status_lock(self, pull_request, merge_extras):
326 326 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
327 327 merge_ref = Reference(
328 328 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
329 329 self.merge_mock.return_value = MergeResponse(
330 330 True, True, merge_ref, MergeFailureReason.NONE)
331 331
332 332 merge_extras['repository'] = pull_request.target_repo.repo_name
333 333
334 334 with pull_request.set_state(PullRequest.STATE_UPDATING):
335 335 assert pull_request.pull_request_state == PullRequest.STATE_UPDATING
336 336 PullRequestModel().merge_repo(
337 337 pull_request, pull_request.author, extras=merge_extras)
338 338 Session().commit()
339 339
340 340 assert pull_request.pull_request_state == PullRequest.STATE_CREATED
341 341
342 342 message = (
343 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
343 u'Merge pull request !{pr_id} from {source_repo} {source_ref_name}'
344 344 u'\n\n {pr_title}'.format(
345 345 pr_id=pull_request.pull_request_id,
346 346 source_repo=safe_unicode(
347 347 pull_request.source_repo.scm_instance().name),
348 348 source_ref_name=pull_request.source_ref_parts.name,
349 349 pr_title=safe_unicode(pull_request.title)
350 350 )
351 351 )
352 352 self.merge_mock.assert_called_with(
353 353 self.repo_id, self.workspace_id,
354 354 pull_request.target_ref_parts,
355 355 pull_request.source_repo.scm_instance(),
356 356 pull_request.source_ref_parts,
357 357 user_name=user.short_contact, user_email=user.email, message=message,
358 358 use_rebase=False, close_branch=False
359 359 )
360 360 self.invalidation_mock.assert_called_once_with(
361 361 pull_request.target_repo.repo_name)
362 362
363 363 self.hook_mock.assert_called_with(
364 364 self.pull_request, self.pull_request.author, 'merge')
365 365
366 366 pull_request = PullRequest.get(pull_request.pull_request_id)
367 367 assert pull_request.merge_rev == '6126b7bfcc82ad2d3deaee22af926b082ce54cc6'
368 368
369 369 def test_merge_failed(self, pull_request, merge_extras):
370 370 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
371 371 merge_ref = Reference(
372 372 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
373 373 self.merge_mock.return_value = MergeResponse(
374 374 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
375 375
376 376 merge_extras['repository'] = pull_request.target_repo.repo_name
377 377 PullRequestModel().merge_repo(
378 378 pull_request, pull_request.author, extras=merge_extras)
379 379 Session().commit()
380 380
381 381 message = (
382 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
382 u'Merge pull request !{pr_id} from {source_repo} {source_ref_name}'
383 383 u'\n\n {pr_title}'.format(
384 384 pr_id=pull_request.pull_request_id,
385 385 source_repo=safe_unicode(
386 386 pull_request.source_repo.scm_instance().name),
387 387 source_ref_name=pull_request.source_ref_parts.name,
388 388 pr_title=safe_unicode(pull_request.title)
389 389 )
390 390 )
391 391 self.merge_mock.assert_called_with(
392 392 self.repo_id, self.workspace_id,
393 393 pull_request.target_ref_parts,
394 394 pull_request.source_repo.scm_instance(),
395 395 pull_request.source_ref_parts,
396 396 user_name=user.short_contact, user_email=user.email, message=message,
397 397 use_rebase=False, close_branch=False
398 398 )
399 399
400 400 pull_request = PullRequest.get(pull_request.pull_request_id)
401 401 assert self.invalidation_mock.called is False
402 402 assert pull_request.merge_rev is None
403 403
404 404 def test_get_commit_ids(self, pull_request):
405 405 # The PR has been not merget yet, so expect an exception
406 406 with pytest.raises(ValueError):
407 407 PullRequestModel()._get_commit_ids(pull_request)
408 408
409 409 # Merge revision is in the revisions list
410 410 pull_request.merge_rev = pull_request.revisions[0]
411 411 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
412 412 assert commit_ids == pull_request.revisions
413 413
414 414 # Merge revision is not in the revisions list
415 415 pull_request.merge_rev = 'f000' * 10
416 416 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
417 417 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
418 418
419 419 def test_get_diff_from_pr_version(self, pull_request):
420 420 source_repo = pull_request.source_repo
421 421 source_ref_id = pull_request.source_ref_parts.commit_id
422 422 target_ref_id = pull_request.target_ref_parts.commit_id
423 423 diff = PullRequestModel()._get_diff_from_pr_or_version(
424 424 source_repo, source_ref_id, target_ref_id,
425 425 hide_whitespace_changes=False, diff_context=6)
426 426 assert 'file_1' in diff.raw
427 427
428 428 def test_generate_title_returns_unicode(self):
429 429 title = PullRequestModel().generate_pullrequest_title(
430 430 source='source-dummy',
431 431 source_ref='source-ref-dummy',
432 432 target='target-dummy',
433 433 )
434 434 assert type(title) == unicode
435 435
436 436
437 437 @pytest.mark.usefixtures('config_stub')
438 438 class TestIntegrationMerge(object):
439 439 @pytest.mark.parametrize('extra_config', (
440 440 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
441 441 ))
442 442 def test_merge_triggers_push_hooks(
443 443 self, pr_util, user_admin, capture_rcextensions, merge_extras,
444 444 extra_config):
445 445
446 446 pull_request = pr_util.create_pull_request(
447 447 approved=True, mergeable=True)
448 448 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
449 449 merge_extras['repository'] = pull_request.target_repo.repo_name
450 450 Session().commit()
451 451
452 452 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
453 453 merge_state = PullRequestModel().merge_repo(
454 454 pull_request, user_admin, extras=merge_extras)
455 455 Session().commit()
456 456
457 457 assert merge_state.executed
458 458 assert '_pre_push_hook' in capture_rcextensions
459 459 assert '_push_hook' in capture_rcextensions
460 460
461 461 def test_merge_can_be_rejected_by_pre_push_hook(
462 462 self, pr_util, user_admin, capture_rcextensions, merge_extras):
463 463 pull_request = pr_util.create_pull_request(
464 464 approved=True, mergeable=True)
465 465 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
466 466 merge_extras['repository'] = pull_request.target_repo.repo_name
467 467 Session().commit()
468 468
469 469 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
470 470 pre_pull.side_effect = RepositoryError("Disallow push!")
471 471 merge_status = PullRequestModel().merge_repo(
472 472 pull_request, user_admin, extras=merge_extras)
473 473 Session().commit()
474 474
475 475 assert not merge_status.executed
476 476 assert 'pre_push' not in capture_rcextensions
477 477 assert 'post_push' not in capture_rcextensions
478 478
479 479 def test_merge_fails_if_target_is_locked(
480 480 self, pr_util, user_regular, merge_extras):
481 481 pull_request = pr_util.create_pull_request(
482 482 approved=True, mergeable=True)
483 483 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
484 484 pull_request.target_repo.locked = locked_by
485 485 # TODO: johbo: Check if this can work based on the database, currently
486 486 # all data is pre-computed, that's why just updating the DB is not
487 487 # enough.
488 488 merge_extras['locked_by'] = locked_by
489 489 merge_extras['repository'] = pull_request.target_repo.repo_name
490 490 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
491 491 Session().commit()
492 492 merge_status = PullRequestModel().merge_repo(
493 493 pull_request, user_regular, extras=merge_extras)
494 494 Session().commit()
495 495
496 496 assert not merge_status.executed
497 497
498 498
499 499 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
500 500 (False, 1, 0),
501 501 (True, 0, 1),
502 502 ])
503 503 def test_outdated_comments(
504 504 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
505 505 pull_request = pr_util.create_pull_request()
506 506 pr_util.create_inline_comment(file_path='not_in_updated_diff')
507 507
508 508 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
509 509 pr_util.add_one_commit()
510 510 assert_inline_comments(
511 511 pull_request, visible=inlines_count, outdated=outdated_count)
512 512 outdated_comment_mock.assert_called_with(pull_request)
513 513
514 514
515 515 @pytest.mark.parametrize('mr_type, expected_msg', [
516 516 (MergeFailureReason.NONE,
517 517 'This pull request can be automatically merged.'),
518 518 (MergeFailureReason.UNKNOWN,
519 519 'This pull request cannot be merged because of an unhandled exception. CRASH'),
520 520 (MergeFailureReason.MERGE_FAILED,
521 521 'This pull request cannot be merged because of merge conflicts.'),
522 522 (MergeFailureReason.PUSH_FAILED,
523 523 'This pull request could not be merged because push to target:`some-repo@merge_commit` failed.'),
524 524 (MergeFailureReason.TARGET_IS_NOT_HEAD,
525 525 'This pull request cannot be merged because the target `ref_name` is not a head.'),
526 526 (MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES,
527 527 'This pull request cannot be merged because the source contains more branches than the target.'),
528 528 (MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
529 529 'This pull request cannot be merged because the target `ref_name` has multiple heads: `a,b,c`.'),
530 530 (MergeFailureReason.TARGET_IS_LOCKED,
531 531 'This pull request cannot be merged because the target repository is locked by user:123.'),
532 532 (MergeFailureReason.MISSING_TARGET_REF,
533 533 'This pull request cannot be merged because the target reference `ref_name` is missing.'),
534 534 (MergeFailureReason.MISSING_SOURCE_REF,
535 535 'This pull request cannot be merged because the source reference `ref_name` is missing.'),
536 536 (MergeFailureReason.SUBREPO_MERGE_FAILED,
537 537 'This pull request cannot be merged because of conflicts related to sub repositories.'),
538 538
539 539 ])
540 540 def test_merge_response_message(mr_type, expected_msg):
541 541 merge_ref = Reference('type', 'ref_name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
542 542 metadata = {
543 543 'exception': "CRASH",
544 544 'target': 'some-repo',
545 545 'merge_commit': 'merge_commit',
546 546 'target_ref': merge_ref,
547 547 'source_ref': merge_ref,
548 548 'heads': ','.join(['a', 'b', 'c']),
549 549 'locked_by': 'user:123'}
550 550
551 551 merge_response = MergeResponse(True, True, merge_ref, mr_type, metadata=metadata)
552 552 assert merge_response.merge_status_message == expected_msg
553 553
554 554
555 555 @pytest.fixture()
556 556 def merge_extras(user_regular):
557 557 """
558 558 Context for the vcs operation when running a merge.
559 559 """
560 560 extras = {
561 561 'ip': '127.0.0.1',
562 562 'username': user_regular.username,
563 563 'user_id': user_regular.user_id,
564 564 'action': 'push',
565 565 'repository': 'fake_target_repo_name',
566 566 'scm': 'git',
567 567 'config': 'fake_config_ini_path',
568 568 'repo_store': '',
569 569 'make_lock': None,
570 570 'locked_by': [None, None, None],
571 571 'server_url': 'http://test.example.com:5000',
572 572 'hooks': ['push', 'pull'],
573 573 'is_shadow_repo': False,
574 574 }
575 575 return extras
576 576
577 577
578 578 @pytest.mark.usefixtures('config_stub')
579 579 class TestUpdateCommentHandling(object):
580 580
581 581 @pytest.fixture(autouse=True, scope='class')
582 582 def enable_outdated_comments(self, request, baseapp):
583 583 config_patch = mock.patch.dict(
584 584 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
585 585 config_patch.start()
586 586
587 587 @request.addfinalizer
588 588 def cleanup():
589 589 config_patch.stop()
590 590
591 591 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
592 592 commits = [
593 593 {'message': 'a'},
594 594 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
595 595 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
596 596 ]
597 597 pull_request = pr_util.create_pull_request(
598 598 commits=commits, target_head='a', source_head='b', revisions=['b'])
599 599 pr_util.create_inline_comment(file_path='file_b')
600 600 pr_util.add_one_commit(head='c')
601 601
602 602 assert_inline_comments(pull_request, visible=1, outdated=0)
603 603
604 604 def test_comment_stays_unflagged_on_change_above(self, pr_util):
605 605 original_content = ''.join(
606 606 ['line {}\n'.format(x) for x in range(1, 11)])
607 607 updated_content = 'new_line_at_top\n' + original_content
608 608 commits = [
609 609 {'message': 'a'},
610 610 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
611 611 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
612 612 ]
613 613 pull_request = pr_util.create_pull_request(
614 614 commits=commits, target_head='a', source_head='b', revisions=['b'])
615 615
616 616 with outdated_comments_patcher():
617 617 comment = pr_util.create_inline_comment(
618 618 line_no=u'n8', file_path='file_b')
619 619 pr_util.add_one_commit(head='c')
620 620
621 621 assert_inline_comments(pull_request, visible=1, outdated=0)
622 622 assert comment.line_no == u'n9'
623 623
624 624 def test_comment_stays_unflagged_on_change_below(self, pr_util):
625 625 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
626 626 updated_content = original_content + 'new_line_at_end\n'
627 627 commits = [
628 628 {'message': 'a'},
629 629 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
630 630 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
631 631 ]
632 632 pull_request = pr_util.create_pull_request(
633 633 commits=commits, target_head='a', source_head='b', revisions=['b'])
634 634 pr_util.create_inline_comment(file_path='file_b')
635 635 pr_util.add_one_commit(head='c')
636 636
637 637 assert_inline_comments(pull_request, visible=1, outdated=0)
638 638
639 639 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
640 640 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
641 641 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
642 642 change_lines = list(base_lines)
643 643 change_lines.insert(6, 'line 6a added\n')
644 644
645 645 # Changes on the last line of sight
646 646 update_lines = list(change_lines)
647 647 update_lines[0] = 'line 1 changed\n'
648 648 update_lines[-1] = 'line 12 changed\n'
649 649
650 650 def file_b(lines):
651 651 return FileNode('file_b', ''.join(lines))
652 652
653 653 commits = [
654 654 {'message': 'a', 'added': [file_b(base_lines)]},
655 655 {'message': 'b', 'changed': [file_b(change_lines)]},
656 656 {'message': 'c', 'changed': [file_b(update_lines)]},
657 657 ]
658 658
659 659 pull_request = pr_util.create_pull_request(
660 660 commits=commits, target_head='a', source_head='b', revisions=['b'])
661 661 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
662 662
663 663 with outdated_comments_patcher():
664 664 pr_util.add_one_commit(head='c')
665 665 assert_inline_comments(pull_request, visible=0, outdated=1)
666 666
667 667 @pytest.mark.parametrize("change, content", [
668 668 ('changed', 'changed\n'),
669 669 ('removed', ''),
670 670 ], ids=['changed', 'removed'])
671 671 def test_comment_flagged_on_change(self, pr_util, change, content):
672 672 commits = [
673 673 {'message': 'a'},
674 674 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
675 675 {'message': 'c', change: [FileNode('file_b', content)]},
676 676 ]
677 677 pull_request = pr_util.create_pull_request(
678 678 commits=commits, target_head='a', source_head='b', revisions=['b'])
679 679 pr_util.create_inline_comment(file_path='file_b')
680 680
681 681 with outdated_comments_patcher():
682 682 pr_util.add_one_commit(head='c')
683 683 assert_inline_comments(pull_request, visible=0, outdated=1)
684 684
685 685
686 686 @pytest.mark.usefixtures('config_stub')
687 687 class TestUpdateChangedFiles(object):
688 688
689 689 def test_no_changes_on_unchanged_diff(self, pr_util):
690 690 commits = [
691 691 {'message': 'a'},
692 692 {'message': 'b',
693 693 'added': [FileNode('file_b', 'test_content b\n')]},
694 694 {'message': 'c',
695 695 'added': [FileNode('file_c', 'test_content c\n')]},
696 696 ]
697 697 # open a PR from a to b, adding file_b
698 698 pull_request = pr_util.create_pull_request(
699 699 commits=commits, target_head='a', source_head='b', revisions=['b'],
700 700 name_suffix='per-file-review')
701 701
702 702 # modify PR adding new file file_c
703 703 pr_util.add_one_commit(head='c')
704 704
705 705 assert_pr_file_changes(
706 706 pull_request,
707 707 added=['file_c'],
708 708 modified=[],
709 709 removed=[])
710 710
711 711 def test_modify_and_undo_modification_diff(self, pr_util):
712 712 commits = [
713 713 {'message': 'a'},
714 714 {'message': 'b',
715 715 'added': [FileNode('file_b', 'test_content b\n')]},
716 716 {'message': 'c',
717 717 'changed': [FileNode('file_b', 'test_content b modified\n')]},
718 718 {'message': 'd',
719 719 'changed': [FileNode('file_b', 'test_content b\n')]},
720 720 ]
721 721 # open a PR from a to b, adding file_b
722 722 pull_request = pr_util.create_pull_request(
723 723 commits=commits, target_head='a', source_head='b', revisions=['b'],
724 724 name_suffix='per-file-review')
725 725
726 726 # modify PR modifying file file_b
727 727 pr_util.add_one_commit(head='c')
728 728
729 729 assert_pr_file_changes(
730 730 pull_request,
731 731 added=[],
732 732 modified=['file_b'],
733 733 removed=[])
734 734
735 735 # move the head again to d, which rollbacks change,
736 736 # meaning we should indicate no changes
737 737 pr_util.add_one_commit(head='d')
738 738
739 739 assert_pr_file_changes(
740 740 pull_request,
741 741 added=[],
742 742 modified=[],
743 743 removed=[])
744 744
745 745 def test_updated_all_files_in_pr(self, pr_util):
746 746 commits = [
747 747 {'message': 'a'},
748 748 {'message': 'b', 'added': [
749 749 FileNode('file_a', 'test_content a\n'),
750 750 FileNode('file_b', 'test_content b\n'),
751 751 FileNode('file_c', 'test_content c\n')]},
752 752 {'message': 'c', 'changed': [
753 753 FileNode('file_a', 'test_content a changed\n'),
754 754 FileNode('file_b', 'test_content b changed\n'),
755 755 FileNode('file_c', 'test_content c changed\n')]},
756 756 ]
757 757 # open a PR from a to b, changing 3 files
758 758 pull_request = pr_util.create_pull_request(
759 759 commits=commits, target_head='a', source_head='b', revisions=['b'],
760 760 name_suffix='per-file-review')
761 761
762 762 pr_util.add_one_commit(head='c')
763 763
764 764 assert_pr_file_changes(
765 765 pull_request,
766 766 added=[],
767 767 modified=['file_a', 'file_b', 'file_c'],
768 768 removed=[])
769 769
770 770 def test_updated_and_removed_all_files_in_pr(self, pr_util):
771 771 commits = [
772 772 {'message': 'a'},
773 773 {'message': 'b', 'added': [
774 774 FileNode('file_a', 'test_content a\n'),
775 775 FileNode('file_b', 'test_content b\n'),
776 776 FileNode('file_c', 'test_content c\n')]},
777 777 {'message': 'c', 'removed': [
778 778 FileNode('file_a', 'test_content a changed\n'),
779 779 FileNode('file_b', 'test_content b changed\n'),
780 780 FileNode('file_c', 'test_content c changed\n')]},
781 781 ]
782 782 # open a PR from a to b, removing 3 files
783 783 pull_request = pr_util.create_pull_request(
784 784 commits=commits, target_head='a', source_head='b', revisions=['b'],
785 785 name_suffix='per-file-review')
786 786
787 787 pr_util.add_one_commit(head='c')
788 788
789 789 assert_pr_file_changes(
790 790 pull_request,
791 791 added=[],
792 792 modified=[],
793 793 removed=['file_a', 'file_b', 'file_c'])
794 794
795 795
796 796 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
797 797 model = PullRequestModel()
798 798 pull_request = pr_util.create_pull_request()
799 799 pr_util.update_source_repository()
800 800
801 801 model.update_commits(pull_request)
802 802
803 803 # Expect that it has a version entry now
804 804 assert len(model.get_versions(pull_request)) == 1
805 805
806 806
807 807 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
808 808 pull_request = pr_util.create_pull_request()
809 809 model = PullRequestModel()
810 810 model.update_commits(pull_request)
811 811
812 812 # Expect that it still has no versions
813 813 assert len(model.get_versions(pull_request)) == 0
814 814
815 815
816 816 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
817 817 model = PullRequestModel()
818 818 pull_request = pr_util.create_pull_request()
819 819 comment = pr_util.create_comment()
820 820 pr_util.update_source_repository()
821 821
822 822 model.update_commits(pull_request)
823 823
824 824 # Expect that the comment is linked to the pr version now
825 825 assert comment.pull_request_version == model.get_versions(pull_request)[0]
826 826
827 827
828 828 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
829 829 model = PullRequestModel()
830 830 pull_request = pr_util.create_pull_request()
831 831 pr_util.update_source_repository()
832 832 pr_util.update_source_repository()
833 833
834 834 model.update_commits(pull_request)
835 835
836 836 # Expect to find a new comment about the change
837 837 expected_message = textwrap.dedent(
838 838 """\
839 839 Pull request updated. Auto status change to |under_review|
840 840
841 841 .. role:: added
842 842 .. role:: removed
843 843 .. parsed-literal::
844 844
845 845 Changed commits:
846 846 * :added:`1 added`
847 847 * :removed:`0 removed`
848 848
849 849 Changed files:
850 850 * `A file_2 <#a_c--92ed3b5f07b4>`_
851 851
852 852 .. |under_review| replace:: *"Under Review"*"""
853 853 )
854 854 pull_request_comments = sorted(
855 855 pull_request.comments, key=lambda c: c.modified_at)
856 856 update_comment = pull_request_comments[-1]
857 857 assert update_comment.text == expected_message
858 858
859 859
860 860 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
861 861 pull_request = pr_util.create_pull_request()
862 862
863 863 # Avoiding default values
864 864 pull_request.status = PullRequest.STATUS_CLOSED
865 865 pull_request._last_merge_source_rev = "0" * 40
866 866 pull_request._last_merge_target_rev = "1" * 40
867 867 pull_request.last_merge_status = 1
868 868 pull_request.merge_rev = "2" * 40
869 869
870 870 # Remember automatic values
871 871 created_on = pull_request.created_on
872 872 updated_on = pull_request.updated_on
873 873
874 874 # Create a new version of the pull request
875 875 version = PullRequestModel()._create_version_from_snapshot(pull_request)
876 876
877 877 # Check attributes
878 878 assert version.title == pr_util.create_parameters['title']
879 879 assert version.description == pr_util.create_parameters['description']
880 880 assert version.status == PullRequest.STATUS_CLOSED
881 881
882 882 # versions get updated created_on
883 883 assert version.created_on != created_on
884 884
885 885 assert version.updated_on == updated_on
886 886 assert version.user_id == pull_request.user_id
887 887 assert version.revisions == pr_util.create_parameters['revisions']
888 888 assert version.source_repo == pr_util.source_repository
889 889 assert version.source_ref == pr_util.create_parameters['source_ref']
890 890 assert version.target_repo == pr_util.target_repository
891 891 assert version.target_ref == pr_util.create_parameters['target_ref']
892 892 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
893 893 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
894 894 assert version.last_merge_status == pull_request.last_merge_status
895 895 assert version.merge_rev == pull_request.merge_rev
896 896 assert version.pull_request == pull_request
897 897
898 898
899 899 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
900 900 version1 = pr_util.create_version_of_pull_request()
901 901 comment_linked = pr_util.create_comment(linked_to=version1)
902 902 comment_unlinked = pr_util.create_comment()
903 903 version2 = pr_util.create_version_of_pull_request()
904 904
905 905 PullRequestModel()._link_comments_to_version(version2)
906 906 Session().commit()
907 907
908 908 # Expect that only the new comment is linked to version2
909 909 assert (
910 910 comment_unlinked.pull_request_version_id ==
911 911 version2.pull_request_version_id)
912 912 assert (
913 913 comment_linked.pull_request_version_id ==
914 914 version1.pull_request_version_id)
915 915 assert (
916 916 comment_unlinked.pull_request_version_id !=
917 917 comment_linked.pull_request_version_id)
918 918
919 919
920 920 def test_calculate_commits():
921 921 old_ids = [1, 2, 3]
922 922 new_ids = [1, 3, 4, 5]
923 923 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
924 924 assert change.added == [4, 5]
925 925 assert change.common == [1, 3]
926 926 assert change.removed == [2]
927 927 assert change.total == [1, 3, 4, 5]
928 928
929 929
930 930 def assert_inline_comments(pull_request, visible=None, outdated=None):
931 931 if visible is not None:
932 932 inline_comments = CommentsModel().get_inline_comments(
933 933 pull_request.target_repo.repo_id, pull_request=pull_request)
934 934 inline_cnt = CommentsModel().get_inline_comments_count(
935 935 inline_comments)
936 936 assert inline_cnt == visible
937 937 if outdated is not None:
938 938 outdated_comments = CommentsModel().get_outdated_comments(
939 939 pull_request.target_repo.repo_id, pull_request)
940 940 assert len(outdated_comments) == outdated
941 941
942 942
943 943 def assert_pr_file_changes(
944 944 pull_request, added=None, modified=None, removed=None):
945 945 pr_versions = PullRequestModel().get_versions(pull_request)
946 946 # always use first version, ie original PR to calculate changes
947 947 pull_request_version = pr_versions[0]
948 948 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
949 949 pull_request, pull_request_version)
950 950 file_changes = PullRequestModel()._calculate_file_changes(
951 951 old_diff_data, new_diff_data)
952 952
953 953 assert added == file_changes.added, \
954 954 'expected added:%s vs value:%s' % (added, file_changes.added)
955 955 assert modified == file_changes.modified, \
956 956 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
957 957 assert removed == file_changes.removed, \
958 958 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
959 959
960 960
961 961 def outdated_comments_patcher(use_outdated=True):
962 962 return mock.patch.object(
963 963 CommentsModel, 'use_outdated_comments',
964 964 return_value=use_outdated)
General Comments 0
You need to be logged in to leave comments. Login now