##// END OF EJS Templates
pull-requests: add explicit CLOSE pr action instead of closed status from selector....
marcink -
r1445:934edf37 default
parent child Browse files
Show More
@@ -1,1054 +1,1054 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69
69
70 def __before__(self):
70 def __before__(self):
71 super(PullrequestsController, self).__before__()
71 super(PullrequestsController, self).__before__()
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
72
74
73 def _extract_ordering(self, request):
75 def _extract_ordering(self, request):
74 column_index = safe_int(request.GET.get('order[0][column]'))
76 column_index = safe_int(request.GET.get('order[0][column]'))
75 order_dir = request.GET.get('order[0][dir]', 'desc')
77 order_dir = request.GET.get('order[0][dir]', 'desc')
76 order_by = request.GET.get(
78 order_by = request.GET.get(
77 'columns[%s][data][sort]' % column_index, 'name_raw')
79 'columns[%s][data][sort]' % column_index, 'name_raw')
78 return order_by, order_dir
80 return order_by, order_dir
79
81
80 @LoginRequired()
82 @LoginRequired()
81 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
83 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
82 'repository.admin')
84 'repository.admin')
83 @HasAcceptedRepoType('git', 'hg')
85 @HasAcceptedRepoType('git', 'hg')
84 def show_all(self, repo_name):
86 def show_all(self, repo_name):
85 # filter types
87 # filter types
86 c.active = 'open'
88 c.active = 'open'
87 c.source = str2bool(request.GET.get('source'))
89 c.source = str2bool(request.GET.get('source'))
88 c.closed = str2bool(request.GET.get('closed'))
90 c.closed = str2bool(request.GET.get('closed'))
89 c.my = str2bool(request.GET.get('my'))
91 c.my = str2bool(request.GET.get('my'))
90 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
92 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
91 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
93 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
92 c.repo_name = repo_name
94 c.repo_name = repo_name
93
95
94 opened_by = None
96 opened_by = None
95 if c.my:
97 if c.my:
96 c.active = 'my'
98 c.active = 'my'
97 opened_by = [c.rhodecode_user.user_id]
99 opened_by = [c.rhodecode_user.user_id]
98
100
99 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
101 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
100 if c.closed:
102 if c.closed:
101 c.active = 'closed'
103 c.active = 'closed'
102 statuses = [PullRequest.STATUS_CLOSED]
104 statuses = [PullRequest.STATUS_CLOSED]
103
105
104 if c.awaiting_review and not c.source:
106 if c.awaiting_review and not c.source:
105 c.active = 'awaiting'
107 c.active = 'awaiting'
106 if c.source and not c.awaiting_review:
108 if c.source and not c.awaiting_review:
107 c.active = 'source'
109 c.active = 'source'
108 if c.awaiting_my_review:
110 if c.awaiting_my_review:
109 c.active = 'awaiting_my'
111 c.active = 'awaiting_my'
110
112
111 data = self._get_pull_requests_list(
113 data = self._get_pull_requests_list(
112 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
114 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
113 if not request.is_xhr:
115 if not request.is_xhr:
114 c.data = json.dumps(data['data'])
116 c.data = json.dumps(data['data'])
115 c.records_total = data['recordsTotal']
117 c.records_total = data['recordsTotal']
116 return render('/pullrequests/pullrequests.mako')
118 return render('/pullrequests/pullrequests.mako')
117 else:
119 else:
118 return json.dumps(data)
120 return json.dumps(data)
119
121
120 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
122 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
121 # pagination
123 # pagination
122 start = safe_int(request.GET.get('start'), 0)
124 start = safe_int(request.GET.get('start'), 0)
123 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
125 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
124 order_by, order_dir = self._extract_ordering(request)
126 order_by, order_dir = self._extract_ordering(request)
125
127
126 if c.awaiting_review:
128 if c.awaiting_review:
127 pull_requests = PullRequestModel().get_awaiting_review(
129 pull_requests = PullRequestModel().get_awaiting_review(
128 repo_name, source=c.source, opened_by=opened_by,
130 repo_name, source=c.source, opened_by=opened_by,
129 statuses=statuses, offset=start, length=length,
131 statuses=statuses, offset=start, length=length,
130 order_by=order_by, order_dir=order_dir)
132 order_by=order_by, order_dir=order_dir)
131 pull_requests_total_count = PullRequestModel(
133 pull_requests_total_count = PullRequestModel(
132 ).count_awaiting_review(
134 ).count_awaiting_review(
133 repo_name, source=c.source, statuses=statuses,
135 repo_name, source=c.source, statuses=statuses,
134 opened_by=opened_by)
136 opened_by=opened_by)
135 elif c.awaiting_my_review:
137 elif c.awaiting_my_review:
136 pull_requests = PullRequestModel().get_awaiting_my_review(
138 pull_requests = PullRequestModel().get_awaiting_my_review(
137 repo_name, source=c.source, opened_by=opened_by,
139 repo_name, source=c.source, opened_by=opened_by,
138 user_id=c.rhodecode_user.user_id, statuses=statuses,
140 user_id=c.rhodecode_user.user_id, statuses=statuses,
139 offset=start, length=length, order_by=order_by,
141 offset=start, length=length, order_by=order_by,
140 order_dir=order_dir)
142 order_dir=order_dir)
141 pull_requests_total_count = PullRequestModel(
143 pull_requests_total_count = PullRequestModel(
142 ).count_awaiting_my_review(
144 ).count_awaiting_my_review(
143 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
145 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
144 statuses=statuses, opened_by=opened_by)
146 statuses=statuses, opened_by=opened_by)
145 else:
147 else:
146 pull_requests = PullRequestModel().get_all(
148 pull_requests = PullRequestModel().get_all(
147 repo_name, source=c.source, opened_by=opened_by,
149 repo_name, source=c.source, opened_by=opened_by,
148 statuses=statuses, offset=start, length=length,
150 statuses=statuses, offset=start, length=length,
149 order_by=order_by, order_dir=order_dir)
151 order_by=order_by, order_dir=order_dir)
150 pull_requests_total_count = PullRequestModel().count_all(
152 pull_requests_total_count = PullRequestModel().count_all(
151 repo_name, source=c.source, statuses=statuses,
153 repo_name, source=c.source, statuses=statuses,
152 opened_by=opened_by)
154 opened_by=opened_by)
153
155
154 from rhodecode.lib.utils import PartialRenderer
156 from rhodecode.lib.utils import PartialRenderer
155 _render = PartialRenderer('data_table/_dt_elements.mako')
157 _render = PartialRenderer('data_table/_dt_elements.mako')
156 data = []
158 data = []
157 for pr in pull_requests:
159 for pr in pull_requests:
158 comments = CommentsModel().get_all_comments(
160 comments = CommentsModel().get_all_comments(
159 c.rhodecode_db_repo.repo_id, pull_request=pr)
161 c.rhodecode_db_repo.repo_id, pull_request=pr)
160
162
161 data.append({
163 data.append({
162 'name': _render('pullrequest_name',
164 'name': _render('pullrequest_name',
163 pr.pull_request_id, pr.target_repo.repo_name),
165 pr.pull_request_id, pr.target_repo.repo_name),
164 'name_raw': pr.pull_request_id,
166 'name_raw': pr.pull_request_id,
165 'status': _render('pullrequest_status',
167 'status': _render('pullrequest_status',
166 pr.calculated_review_status()),
168 pr.calculated_review_status()),
167 'title': _render(
169 'title': _render(
168 'pullrequest_title', pr.title, pr.description),
170 'pullrequest_title', pr.title, pr.description),
169 'description': h.escape(pr.description),
171 'description': h.escape(pr.description),
170 'updated_on': _render('pullrequest_updated_on',
172 'updated_on': _render('pullrequest_updated_on',
171 h.datetime_to_time(pr.updated_on)),
173 h.datetime_to_time(pr.updated_on)),
172 'updated_on_raw': h.datetime_to_time(pr.updated_on),
174 'updated_on_raw': h.datetime_to_time(pr.updated_on),
173 'created_on': _render('pullrequest_updated_on',
175 'created_on': _render('pullrequest_updated_on',
174 h.datetime_to_time(pr.created_on)),
176 h.datetime_to_time(pr.created_on)),
175 'created_on_raw': h.datetime_to_time(pr.created_on),
177 'created_on_raw': h.datetime_to_time(pr.created_on),
176 'author': _render('pullrequest_author',
178 'author': _render('pullrequest_author',
177 pr.author.full_contact, ),
179 pr.author.full_contact, ),
178 'author_raw': pr.author.full_name,
180 'author_raw': pr.author.full_name,
179 'comments': _render('pullrequest_comments', len(comments)),
181 'comments': _render('pullrequest_comments', len(comments)),
180 'comments_raw': len(comments),
182 'comments_raw': len(comments),
181 'closed': pr.is_closed(),
183 'closed': pr.is_closed(),
182 })
184 })
183 # json used to render the grid
185 # json used to render the grid
184 data = ({
186 data = ({
185 'data': data,
187 'data': data,
186 'recordsTotal': pull_requests_total_count,
188 'recordsTotal': pull_requests_total_count,
187 'recordsFiltered': pull_requests_total_count,
189 'recordsFiltered': pull_requests_total_count,
188 })
190 })
189 return data
191 return data
190
192
191 @LoginRequired()
193 @LoginRequired()
192 @NotAnonymous()
194 @NotAnonymous()
193 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
195 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
194 'repository.admin')
196 'repository.admin')
195 @HasAcceptedRepoType('git', 'hg')
197 @HasAcceptedRepoType('git', 'hg')
196 def index(self):
198 def index(self):
197 source_repo = c.rhodecode_db_repo
199 source_repo = c.rhodecode_db_repo
198
200
199 try:
201 try:
200 source_repo.scm_instance().get_commit()
202 source_repo.scm_instance().get_commit()
201 except EmptyRepositoryError:
203 except EmptyRepositoryError:
202 h.flash(h.literal(_('There are no commits yet')),
204 h.flash(h.literal(_('There are no commits yet')),
203 category='warning')
205 category='warning')
204 redirect(url('summary_home', repo_name=source_repo.repo_name))
206 redirect(url('summary_home', repo_name=source_repo.repo_name))
205
207
206 commit_id = request.GET.get('commit')
208 commit_id = request.GET.get('commit')
207 branch_ref = request.GET.get('branch')
209 branch_ref = request.GET.get('branch')
208 bookmark_ref = request.GET.get('bookmark')
210 bookmark_ref = request.GET.get('bookmark')
209
211
210 try:
212 try:
211 source_repo_data = PullRequestModel().generate_repo_data(
213 source_repo_data = PullRequestModel().generate_repo_data(
212 source_repo, commit_id=commit_id,
214 source_repo, commit_id=commit_id,
213 branch=branch_ref, bookmark=bookmark_ref)
215 branch=branch_ref, bookmark=bookmark_ref)
214 except CommitDoesNotExistError as e:
216 except CommitDoesNotExistError as e:
215 log.exception(e)
217 log.exception(e)
216 h.flash(_('Commit does not exist'), 'error')
218 h.flash(_('Commit does not exist'), 'error')
217 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
219 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
218
220
219 default_target_repo = source_repo
221 default_target_repo = source_repo
220
222
221 if source_repo.parent:
223 if source_repo.parent:
222 parent_vcs_obj = source_repo.parent.scm_instance()
224 parent_vcs_obj = source_repo.parent.scm_instance()
223 if parent_vcs_obj and not parent_vcs_obj.is_empty():
225 if parent_vcs_obj and not parent_vcs_obj.is_empty():
224 # change default if we have a parent repo
226 # change default if we have a parent repo
225 default_target_repo = source_repo.parent
227 default_target_repo = source_repo.parent
226
228
227 target_repo_data = PullRequestModel().generate_repo_data(
229 target_repo_data = PullRequestModel().generate_repo_data(
228 default_target_repo)
230 default_target_repo)
229
231
230 selected_source_ref = source_repo_data['refs']['selected_ref']
232 selected_source_ref = source_repo_data['refs']['selected_ref']
231
233
232 title_source_ref = selected_source_ref.split(':', 2)[1]
234 title_source_ref = selected_source_ref.split(':', 2)[1]
233 c.default_title = PullRequestModel().generate_pullrequest_title(
235 c.default_title = PullRequestModel().generate_pullrequest_title(
234 source=source_repo.repo_name,
236 source=source_repo.repo_name,
235 source_ref=title_source_ref,
237 source_ref=title_source_ref,
236 target=default_target_repo.repo_name
238 target=default_target_repo.repo_name
237 )
239 )
238
240
239 c.default_repo_data = {
241 c.default_repo_data = {
240 'source_repo_name': source_repo.repo_name,
242 'source_repo_name': source_repo.repo_name,
241 'source_refs_json': json.dumps(source_repo_data),
243 'source_refs_json': json.dumps(source_repo_data),
242 'target_repo_name': default_target_repo.repo_name,
244 'target_repo_name': default_target_repo.repo_name,
243 'target_refs_json': json.dumps(target_repo_data),
245 'target_refs_json': json.dumps(target_repo_data),
244 }
246 }
245 c.default_source_ref = selected_source_ref
247 c.default_source_ref = selected_source_ref
246
248
247 return render('/pullrequests/pullrequest.mako')
249 return render('/pullrequests/pullrequest.mako')
248
250
249 @LoginRequired()
251 @LoginRequired()
250 @NotAnonymous()
252 @NotAnonymous()
251 @XHRRequired()
253 @XHRRequired()
252 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
254 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
253 'repository.admin')
255 'repository.admin')
254 @jsonify
256 @jsonify
255 def get_repo_refs(self, repo_name, target_repo_name):
257 def get_repo_refs(self, repo_name, target_repo_name):
256 repo = Repository.get_by_repo_name(target_repo_name)
258 repo = Repository.get_by_repo_name(target_repo_name)
257 if not repo:
259 if not repo:
258 raise HTTPNotFound
260 raise HTTPNotFound
259 return PullRequestModel().generate_repo_data(repo)
261 return PullRequestModel().generate_repo_data(repo)
260
262
261 @LoginRequired()
263 @LoginRequired()
262 @NotAnonymous()
264 @NotAnonymous()
263 @XHRRequired()
265 @XHRRequired()
264 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
266 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
265 'repository.admin')
267 'repository.admin')
266 @jsonify
268 @jsonify
267 def get_repo_destinations(self, repo_name):
269 def get_repo_destinations(self, repo_name):
268 repo = Repository.get_by_repo_name(repo_name)
270 repo = Repository.get_by_repo_name(repo_name)
269 if not repo:
271 if not repo:
270 raise HTTPNotFound
272 raise HTTPNotFound
271 filter_query = request.GET.get('query')
273 filter_query = request.GET.get('query')
272
274
273 query = Repository.query() \
275 query = Repository.query() \
274 .order_by(func.length(Repository.repo_name)) \
276 .order_by(func.length(Repository.repo_name)) \
275 .filter(or_(
277 .filter(or_(
276 Repository.repo_name == repo.repo_name,
278 Repository.repo_name == repo.repo_name,
277 Repository.fork_id == repo.repo_id))
279 Repository.fork_id == repo.repo_id))
278
280
279 if filter_query:
281 if filter_query:
280 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
282 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
281 query = query.filter(
283 query = query.filter(
282 Repository.repo_name.ilike(ilike_expression))
284 Repository.repo_name.ilike(ilike_expression))
283
285
284 add_parent = False
286 add_parent = False
285 if repo.parent:
287 if repo.parent:
286 if filter_query in repo.parent.repo_name:
288 if filter_query in repo.parent.repo_name:
287 parent_vcs_obj = repo.parent.scm_instance()
289 parent_vcs_obj = repo.parent.scm_instance()
288 if parent_vcs_obj and not parent_vcs_obj.is_empty():
290 if parent_vcs_obj and not parent_vcs_obj.is_empty():
289 add_parent = True
291 add_parent = True
290
292
291 limit = 20 - 1 if add_parent else 20
293 limit = 20 - 1 if add_parent else 20
292 all_repos = query.limit(limit).all()
294 all_repos = query.limit(limit).all()
293 if add_parent:
295 if add_parent:
294 all_repos += [repo.parent]
296 all_repos += [repo.parent]
295
297
296 repos = []
298 repos = []
297 for obj in self.scm_model.get_repos(all_repos):
299 for obj in self.scm_model.get_repos(all_repos):
298 repos.append({
300 repos.append({
299 'id': obj['name'],
301 'id': obj['name'],
300 'text': obj['name'],
302 'text': obj['name'],
301 'type': 'repo',
303 'type': 'repo',
302 'obj': obj['dbrepo']
304 'obj': obj['dbrepo']
303 })
305 })
304
306
305 data = {
307 data = {
306 'more': False,
308 'more': False,
307 'results': [{
309 'results': [{
308 'text': _('Repositories'),
310 'text': _('Repositories'),
309 'children': repos
311 'children': repos
310 }] if repos else []
312 }] if repos else []
311 }
313 }
312 return data
314 return data
313
315
314 @LoginRequired()
316 @LoginRequired()
315 @NotAnonymous()
317 @NotAnonymous()
316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 'repository.admin')
319 'repository.admin')
318 @HasAcceptedRepoType('git', 'hg')
320 @HasAcceptedRepoType('git', 'hg')
319 @auth.CSRFRequired()
321 @auth.CSRFRequired()
320 def create(self, repo_name):
322 def create(self, repo_name):
321 repo = Repository.get_by_repo_name(repo_name)
323 repo = Repository.get_by_repo_name(repo_name)
322 if not repo:
324 if not repo:
323 raise HTTPNotFound
325 raise HTTPNotFound
324
326
325 controls = peppercorn.parse(request.POST.items())
327 controls = peppercorn.parse(request.POST.items())
326
328
327 try:
329 try:
328 _form = PullRequestForm(repo.repo_id)().to_python(controls)
330 _form = PullRequestForm(repo.repo_id)().to_python(controls)
329 except formencode.Invalid as errors:
331 except formencode.Invalid as errors:
330 if errors.error_dict.get('revisions'):
332 if errors.error_dict.get('revisions'):
331 msg = 'Revisions: %s' % errors.error_dict['revisions']
333 msg = 'Revisions: %s' % errors.error_dict['revisions']
332 elif errors.error_dict.get('pullrequest_title'):
334 elif errors.error_dict.get('pullrequest_title'):
333 msg = _('Pull request requires a title with min. 3 chars')
335 msg = _('Pull request requires a title with min. 3 chars')
334 else:
336 else:
335 msg = _('Error creating pull request: {}').format(errors)
337 msg = _('Error creating pull request: {}').format(errors)
336 log.exception(msg)
338 log.exception(msg)
337 h.flash(msg, 'error')
339 h.flash(msg, 'error')
338
340
339 # would rather just go back to form ...
341 # would rather just go back to form ...
340 return redirect(url('pullrequest_home', repo_name=repo_name))
342 return redirect(url('pullrequest_home', repo_name=repo_name))
341
343
342 source_repo = _form['source_repo']
344 source_repo = _form['source_repo']
343 source_ref = _form['source_ref']
345 source_ref = _form['source_ref']
344 target_repo = _form['target_repo']
346 target_repo = _form['target_repo']
345 target_ref = _form['target_ref']
347 target_ref = _form['target_ref']
346 commit_ids = _form['revisions'][::-1]
348 commit_ids = _form['revisions'][::-1]
347 reviewers = [
349 reviewers = [
348 (r['user_id'], r['reasons']) for r in _form['review_members']]
350 (r['user_id'], r['reasons']) for r in _form['review_members']]
349
351
350 # find the ancestor for this pr
352 # find the ancestor for this pr
351 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
353 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
352 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
354 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
353
355
354 source_scm = source_db_repo.scm_instance()
356 source_scm = source_db_repo.scm_instance()
355 target_scm = target_db_repo.scm_instance()
357 target_scm = target_db_repo.scm_instance()
356
358
357 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
359 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
358 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
360 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
359
361
360 ancestor = source_scm.get_common_ancestor(
362 ancestor = source_scm.get_common_ancestor(
361 source_commit.raw_id, target_commit.raw_id, target_scm)
363 source_commit.raw_id, target_commit.raw_id, target_scm)
362
364
363 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
365 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
364 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
366 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
365
367
366 pullrequest_title = _form['pullrequest_title']
368 pullrequest_title = _form['pullrequest_title']
367 title_source_ref = source_ref.split(':', 2)[1]
369 title_source_ref = source_ref.split(':', 2)[1]
368 if not pullrequest_title:
370 if not pullrequest_title:
369 pullrequest_title = PullRequestModel().generate_pullrequest_title(
371 pullrequest_title = PullRequestModel().generate_pullrequest_title(
370 source=source_repo,
372 source=source_repo,
371 source_ref=title_source_ref,
373 source_ref=title_source_ref,
372 target=target_repo
374 target=target_repo
373 )
375 )
374
376
375 description = _form['pullrequest_desc']
377 description = _form['pullrequest_desc']
376 try:
378 try:
377 pull_request = PullRequestModel().create(
379 pull_request = PullRequestModel().create(
378 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
380 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
379 target_ref, commit_ids, reviewers, pullrequest_title,
381 target_ref, commit_ids, reviewers, pullrequest_title,
380 description
382 description
381 )
383 )
382 Session().commit()
384 Session().commit()
383 h.flash(_('Successfully opened new pull request'),
385 h.flash(_('Successfully opened new pull request'),
384 category='success')
386 category='success')
385 except Exception as e:
387 except Exception as e:
386 msg = _('Error occurred during sending pull request')
388 msg = _('Error occurred during sending pull request')
387 log.exception(msg)
389 log.exception(msg)
388 h.flash(msg, category='error')
390 h.flash(msg, category='error')
389 return redirect(url('pullrequest_home', repo_name=repo_name))
391 return redirect(url('pullrequest_home', repo_name=repo_name))
390
392
391 return redirect(url('pullrequest_show', repo_name=target_repo,
393 return redirect(url('pullrequest_show', repo_name=target_repo,
392 pull_request_id=pull_request.pull_request_id))
394 pull_request_id=pull_request.pull_request_id))
393
395
394 @LoginRequired()
396 @LoginRequired()
395 @NotAnonymous()
397 @NotAnonymous()
396 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
398 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
397 'repository.admin')
399 'repository.admin')
398 @auth.CSRFRequired()
400 @auth.CSRFRequired()
399 @jsonify
401 @jsonify
400 def update(self, repo_name, pull_request_id):
402 def update(self, repo_name, pull_request_id):
401 pull_request_id = safe_int(pull_request_id)
403 pull_request_id = safe_int(pull_request_id)
402 pull_request = PullRequest.get_or_404(pull_request_id)
404 pull_request = PullRequest.get_or_404(pull_request_id)
403 # only owner or admin can update it
405 # only owner or admin can update it
404 allowed_to_update = PullRequestModel().check_user_update(
406 allowed_to_update = PullRequestModel().check_user_update(
405 pull_request, c.rhodecode_user)
407 pull_request, c.rhodecode_user)
406 if allowed_to_update:
408 if allowed_to_update:
407 controls = peppercorn.parse(request.POST.items())
409 controls = peppercorn.parse(request.POST.items())
408
410
409 if 'review_members' in controls:
411 if 'review_members' in controls:
410 self._update_reviewers(
412 self._update_reviewers(
411 pull_request_id, controls['review_members'])
413 pull_request_id, controls['review_members'])
412 elif str2bool(request.POST.get('update_commits', 'false')):
414 elif str2bool(request.POST.get('update_commits', 'false')):
413 self._update_commits(pull_request)
415 self._update_commits(pull_request)
414 elif str2bool(request.POST.get('close_pull_request', 'false')):
416 elif str2bool(request.POST.get('close_pull_request', 'false')):
415 self._reject_close(pull_request)
417 self._reject_close(pull_request)
416 elif str2bool(request.POST.get('edit_pull_request', 'false')):
418 elif str2bool(request.POST.get('edit_pull_request', 'false')):
417 self._edit_pull_request(pull_request)
419 self._edit_pull_request(pull_request)
418 else:
420 else:
419 raise HTTPBadRequest()
421 raise HTTPBadRequest()
420 return True
422 return True
421 raise HTTPForbidden()
423 raise HTTPForbidden()
422
424
423 def _edit_pull_request(self, pull_request):
425 def _edit_pull_request(self, pull_request):
424 try:
426 try:
425 PullRequestModel().edit(
427 PullRequestModel().edit(
426 pull_request, request.POST.get('title'),
428 pull_request, request.POST.get('title'),
427 request.POST.get('description'))
429 request.POST.get('description'))
428 except ValueError:
430 except ValueError:
429 msg = _(u'Cannot update closed pull requests.')
431 msg = _(u'Cannot update closed pull requests.')
430 h.flash(msg, category='error')
432 h.flash(msg, category='error')
431 return
433 return
432 else:
434 else:
433 Session().commit()
435 Session().commit()
434
436
435 msg = _(u'Pull request title & description updated.')
437 msg = _(u'Pull request title & description updated.')
436 h.flash(msg, category='success')
438 h.flash(msg, category='success')
437 return
439 return
438
440
439 def _update_commits(self, pull_request):
441 def _update_commits(self, pull_request):
440 resp = PullRequestModel().update_commits(pull_request)
442 resp = PullRequestModel().update_commits(pull_request)
441
443
442 if resp.executed:
444 if resp.executed:
443 msg = _(
445 msg = _(
444 u'Pull request updated to "{source_commit_id}" with '
446 u'Pull request updated to "{source_commit_id}" with '
445 u'{count_added} added, {count_removed} removed commits.')
447 u'{count_added} added, {count_removed} removed commits.')
446 msg = msg.format(
448 msg = msg.format(
447 source_commit_id=pull_request.source_ref_parts.commit_id,
449 source_commit_id=pull_request.source_ref_parts.commit_id,
448 count_added=len(resp.changes.added),
450 count_added=len(resp.changes.added),
449 count_removed=len(resp.changes.removed))
451 count_removed=len(resp.changes.removed))
450 h.flash(msg, category='success')
452 h.flash(msg, category='success')
451
453
452 registry = get_current_registry()
454 registry = get_current_registry()
453 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
455 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
454 channelstream_config = rhodecode_plugins.get('channelstream', {})
456 channelstream_config = rhodecode_plugins.get('channelstream', {})
455 if channelstream_config.get('enabled'):
457 if channelstream_config.get('enabled'):
456 message = msg + (
458 message = msg + (
457 ' - <a onclick="window.location.reload()">'
459 ' - <a onclick="window.location.reload()">'
458 '<strong>{}</strong></a>'.format(_('Reload page')))
460 '<strong>{}</strong></a>'.format(_('Reload page')))
459 channel = '/repo${}$/pr/{}'.format(
461 channel = '/repo${}$/pr/{}'.format(
460 pull_request.target_repo.repo_name,
462 pull_request.target_repo.repo_name,
461 pull_request.pull_request_id
463 pull_request.pull_request_id
462 )
464 )
463 payload = {
465 payload = {
464 'type': 'message',
466 'type': 'message',
465 'user': 'system',
467 'user': 'system',
466 'exclude_users': [request.user.username],
468 'exclude_users': [request.user.username],
467 'channel': channel,
469 'channel': channel,
468 'message': {
470 'message': {
469 'message': message,
471 'message': message,
470 'level': 'success',
472 'level': 'success',
471 'topic': '/notifications'
473 'topic': '/notifications'
472 }
474 }
473 }
475 }
474 channelstream_request(
476 channelstream_request(
475 channelstream_config, [payload], '/message',
477 channelstream_config, [payload], '/message',
476 raise_exc=False)
478 raise_exc=False)
477 else:
479 else:
478 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
480 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
479 warning_reasons = [
481 warning_reasons = [
480 UpdateFailureReason.NO_CHANGE,
482 UpdateFailureReason.NO_CHANGE,
481 UpdateFailureReason.WRONG_REF_TPYE,
483 UpdateFailureReason.WRONG_REF_TPYE,
482 ]
484 ]
483 category = 'warning' if resp.reason in warning_reasons else 'error'
485 category = 'warning' if resp.reason in warning_reasons else 'error'
484 h.flash(msg, category=category)
486 h.flash(msg, category=category)
485
487
486 @auth.CSRFRequired()
488 @auth.CSRFRequired()
487 @LoginRequired()
489 @LoginRequired()
488 @NotAnonymous()
490 @NotAnonymous()
489 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
491 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
490 'repository.admin')
492 'repository.admin')
491 def merge(self, repo_name, pull_request_id):
493 def merge(self, repo_name, pull_request_id):
492 """
494 """
493 POST /{repo_name}/pull-request/{pull_request_id}
495 POST /{repo_name}/pull-request/{pull_request_id}
494
496
495 Merge will perform a server-side merge of the specified
497 Merge will perform a server-side merge of the specified
496 pull request, if the pull request is approved and mergeable.
498 pull request, if the pull request is approved and mergeable.
497 After successful merging, the pull request is automatically
499 After successful merging, the pull request is automatically
498 closed, with a relevant comment.
500 closed, with a relevant comment.
499 """
501 """
500 pull_request_id = safe_int(pull_request_id)
502 pull_request_id = safe_int(pull_request_id)
501 pull_request = PullRequest.get_or_404(pull_request_id)
503 pull_request = PullRequest.get_or_404(pull_request_id)
502 user = c.rhodecode_user
504 user = c.rhodecode_user
503
505
504 check = MergeCheck.validate(pull_request, user)
506 check = MergeCheck.validate(pull_request, user)
505 merge_possible = not check.failed
507 merge_possible = not check.failed
506
508
507 for err_type, error_msg in check.errors:
509 for err_type, error_msg in check.errors:
508 h.flash(error_msg, category=err_type)
510 h.flash(error_msg, category=err_type)
509
511
510 if merge_possible:
512 if merge_possible:
511 log.debug("Pre-conditions checked, trying to merge.")
513 log.debug("Pre-conditions checked, trying to merge.")
512 extras = vcs_operation_context(
514 extras = vcs_operation_context(
513 request.environ, repo_name=pull_request.target_repo.repo_name,
515 request.environ, repo_name=pull_request.target_repo.repo_name,
514 username=user.username, action='push',
516 username=user.username, action='push',
515 scm=pull_request.target_repo.repo_type)
517 scm=pull_request.target_repo.repo_type)
516 self._merge_pull_request(pull_request, user, extras)
518 self._merge_pull_request(pull_request, user, extras)
517
519
518 return redirect(url(
520 return redirect(url(
519 'pullrequest_show',
521 'pullrequest_show',
520 repo_name=pull_request.target_repo.repo_name,
522 repo_name=pull_request.target_repo.repo_name,
521 pull_request_id=pull_request.pull_request_id))
523 pull_request_id=pull_request.pull_request_id))
522
524
523 def _merge_pull_request(self, pull_request, user, extras):
525 def _merge_pull_request(self, pull_request, user, extras):
524 merge_resp = PullRequestModel().merge(
526 merge_resp = PullRequestModel().merge(
525 pull_request, user, extras=extras)
527 pull_request, user, extras=extras)
526
528
527 if merge_resp.executed:
529 if merge_resp.executed:
528 log.debug("The merge was successful, closing the pull request.")
530 log.debug("The merge was successful, closing the pull request.")
529 PullRequestModel().close_pull_request(
531 PullRequestModel().close_pull_request(
530 pull_request.pull_request_id, user)
532 pull_request.pull_request_id, user)
531 Session().commit()
533 Session().commit()
532 msg = _('Pull request was successfully merged and closed.')
534 msg = _('Pull request was successfully merged and closed.')
533 h.flash(msg, category='success')
535 h.flash(msg, category='success')
534 else:
536 else:
535 log.debug(
537 log.debug(
536 "The merge was not successful. Merge response: %s",
538 "The merge was not successful. Merge response: %s",
537 merge_resp)
539 merge_resp)
538 msg = PullRequestModel().merge_status_message(
540 msg = PullRequestModel().merge_status_message(
539 merge_resp.failure_reason)
541 merge_resp.failure_reason)
540 h.flash(msg, category='error')
542 h.flash(msg, category='error')
541
543
542 def _update_reviewers(self, pull_request_id, review_members):
544 def _update_reviewers(self, pull_request_id, review_members):
543 reviewers = [
545 reviewers = [
544 (int(r['user_id']), r['reasons']) for r in review_members]
546 (int(r['user_id']), r['reasons']) for r in review_members]
545 PullRequestModel().update_reviewers(pull_request_id, reviewers)
547 PullRequestModel().update_reviewers(pull_request_id, reviewers)
546 Session().commit()
548 Session().commit()
547
549
548 def _reject_close(self, pull_request):
550 def _reject_close(self, pull_request):
549 if pull_request.is_closed():
551 if pull_request.is_closed():
550 raise HTTPForbidden()
552 raise HTTPForbidden()
551
553
552 PullRequestModel().close_pull_request_with_comment(
554 PullRequestModel().close_pull_request_with_comment(
553 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
555 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
554 Session().commit()
556 Session().commit()
555
557
556 @LoginRequired()
558 @LoginRequired()
557 @NotAnonymous()
559 @NotAnonymous()
558 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
560 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
559 'repository.admin')
561 'repository.admin')
560 @auth.CSRFRequired()
562 @auth.CSRFRequired()
561 @jsonify
563 @jsonify
562 def delete(self, repo_name, pull_request_id):
564 def delete(self, repo_name, pull_request_id):
563 pull_request_id = safe_int(pull_request_id)
565 pull_request_id = safe_int(pull_request_id)
564 pull_request = PullRequest.get_or_404(pull_request_id)
566 pull_request = PullRequest.get_or_404(pull_request_id)
565 # only owner can delete it !
567 # only owner can delete it !
566 if pull_request.author.user_id == c.rhodecode_user.user_id:
568 if pull_request.author.user_id == c.rhodecode_user.user_id:
567 PullRequestModel().delete(pull_request)
569 PullRequestModel().delete(pull_request)
568 Session().commit()
570 Session().commit()
569 h.flash(_('Successfully deleted pull request'),
571 h.flash(_('Successfully deleted pull request'),
570 category='success')
572 category='success')
571 return redirect(url('my_account_pullrequests'))
573 return redirect(url('my_account_pullrequests'))
572 raise HTTPForbidden()
574 raise HTTPForbidden()
573
575
574 def _get_pr_version(self, pull_request_id, version=None):
576 def _get_pr_version(self, pull_request_id, version=None):
575 pull_request_id = safe_int(pull_request_id)
577 pull_request_id = safe_int(pull_request_id)
576 at_version = None
578 at_version = None
577
579
578 if version and version == 'latest':
580 if version and version == 'latest':
579 pull_request_ver = PullRequest.get(pull_request_id)
581 pull_request_ver = PullRequest.get(pull_request_id)
580 pull_request_obj = pull_request_ver
582 pull_request_obj = pull_request_ver
581 _org_pull_request_obj = pull_request_obj
583 _org_pull_request_obj = pull_request_obj
582 at_version = 'latest'
584 at_version = 'latest'
583 elif version:
585 elif version:
584 pull_request_ver = PullRequestVersion.get_or_404(version)
586 pull_request_ver = PullRequestVersion.get_or_404(version)
585 pull_request_obj = pull_request_ver
587 pull_request_obj = pull_request_ver
586 _org_pull_request_obj = pull_request_ver.pull_request
588 _org_pull_request_obj = pull_request_ver.pull_request
587 at_version = pull_request_ver.pull_request_version_id
589 at_version = pull_request_ver.pull_request_version_id
588 else:
590 else:
589 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
591 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
590
592
591 pull_request_display_obj = PullRequest.get_pr_display_object(
593 pull_request_display_obj = PullRequest.get_pr_display_object(
592 pull_request_obj, _org_pull_request_obj)
594 pull_request_obj, _org_pull_request_obj)
593
595
594 return _org_pull_request_obj, pull_request_obj, \
596 return _org_pull_request_obj, pull_request_obj, \
595 pull_request_display_obj, at_version
597 pull_request_display_obj, at_version
596
598
597 def _get_diffset(
599 def _get_diffset(
598 self, source_repo, source_ref_id, target_ref_id, target_commit,
600 self, source_repo, source_ref_id, target_ref_id, target_commit,
599 source_commit, diff_limit, file_limit, display_inline_comments):
601 source_commit, diff_limit, file_limit, display_inline_comments):
600 vcs_diff = PullRequestModel().get_diff(
602 vcs_diff = PullRequestModel().get_diff(
601 source_repo, source_ref_id, target_ref_id)
603 source_repo, source_ref_id, target_ref_id)
602
604
603 diff_processor = diffs.DiffProcessor(
605 diff_processor = diffs.DiffProcessor(
604 vcs_diff, format='newdiff', diff_limit=diff_limit,
606 vcs_diff, format='newdiff', diff_limit=diff_limit,
605 file_limit=file_limit, show_full_diff=c.fulldiff)
607 file_limit=file_limit, show_full_diff=c.fulldiff)
606
608
607 _parsed = diff_processor.prepare()
609 _parsed = diff_processor.prepare()
608
610
609 def _node_getter(commit):
611 def _node_getter(commit):
610 def get_node(fname):
612 def get_node(fname):
611 try:
613 try:
612 return commit.get_node(fname)
614 return commit.get_node(fname)
613 except NodeDoesNotExistError:
615 except NodeDoesNotExistError:
614 return None
616 return None
615
617
616 return get_node
618 return get_node
617
619
618 diffset = codeblocks.DiffSet(
620 diffset = codeblocks.DiffSet(
619 repo_name=c.repo_name,
621 repo_name=c.repo_name,
620 source_repo_name=c.source_repo.repo_name,
622 source_repo_name=c.source_repo.repo_name,
621 source_node_getter=_node_getter(target_commit),
623 source_node_getter=_node_getter(target_commit),
622 target_node_getter=_node_getter(source_commit),
624 target_node_getter=_node_getter(source_commit),
623 comments=display_inline_comments
625 comments=display_inline_comments
624 )
626 )
625 diffset = diffset.render_patchset(
627 diffset = diffset.render_patchset(
626 _parsed, target_commit.raw_id, source_commit.raw_id)
628 _parsed, target_commit.raw_id, source_commit.raw_id)
627
629
628 return diffset
630 return diffset
629
631
630 @LoginRequired()
632 @LoginRequired()
631 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
633 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
632 'repository.admin')
634 'repository.admin')
633 def show(self, repo_name, pull_request_id):
635 def show(self, repo_name, pull_request_id):
634 pull_request_id = safe_int(pull_request_id)
636 pull_request_id = safe_int(pull_request_id)
635 version = request.GET.get('version')
637 version = request.GET.get('version')
636 from_version = request.GET.get('from_version') or version
638 from_version = request.GET.get('from_version') or version
637 merge_checks = request.GET.get('merge_checks')
639 merge_checks = request.GET.get('merge_checks')
638 c.fulldiff = str2bool(request.GET.get('fulldiff'))
640 c.fulldiff = str2bool(request.GET.get('fulldiff'))
639
641
640 (pull_request_latest,
642 (pull_request_latest,
641 pull_request_at_ver,
643 pull_request_at_ver,
642 pull_request_display_obj,
644 pull_request_display_obj,
643 at_version) = self._get_pr_version(
645 at_version) = self._get_pr_version(
644 pull_request_id, version=version)
646 pull_request_id, version=version)
645 versions = pull_request_display_obj.versions()
647 versions = pull_request_display_obj.versions()
646
648
647 c.at_version = at_version
649 c.at_version = at_version
648 c.at_version_num = (at_version
650 c.at_version_num = (at_version
649 if at_version and at_version != 'latest'
651 if at_version and at_version != 'latest'
650 else None)
652 else None)
651 c.at_version_pos = ChangesetComment.get_index_from_version(
653 c.at_version_pos = ChangesetComment.get_index_from_version(
652 c.at_version_num, versions)
654 c.at_version_num, versions)
653
655
654 (prev_pull_request_latest,
656 (prev_pull_request_latest,
655 prev_pull_request_at_ver,
657 prev_pull_request_at_ver,
656 prev_pull_request_display_obj,
658 prev_pull_request_display_obj,
657 prev_at_version) = self._get_pr_version(
659 prev_at_version) = self._get_pr_version(
658 pull_request_id, version=from_version)
660 pull_request_id, version=from_version)
659
661
660 c.from_version = prev_at_version
662 c.from_version = prev_at_version
661 c.from_version_num = (prev_at_version
663 c.from_version_num = (prev_at_version
662 if prev_at_version and prev_at_version != 'latest'
664 if prev_at_version and prev_at_version != 'latest'
663 else None)
665 else None)
664 c.from_version_pos = ChangesetComment.get_index_from_version(
666 c.from_version_pos = ChangesetComment.get_index_from_version(
665 c.from_version_num, versions)
667 c.from_version_num, versions)
666
668
667 # define if we're in COMPARE mode or VIEW at version mode
669 # define if we're in COMPARE mode or VIEW at version mode
668 compare = at_version != prev_at_version
670 compare = at_version != prev_at_version
669
671
670 # pull_requests repo_name we opened it against
672 # pull_requests repo_name we opened it against
671 # ie. target_repo must match
673 # ie. target_repo must match
672 if repo_name != pull_request_at_ver.target_repo.repo_name:
674 if repo_name != pull_request_at_ver.target_repo.repo_name:
673 raise HTTPNotFound
675 raise HTTPNotFound
674
676
675 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
677 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
676 pull_request_at_ver)
678 pull_request_at_ver)
677
679
678 c.ancestor = None # empty ancestor hidden in display
680 c.ancestor = None # empty ancestor hidden in display
679 c.pull_request = pull_request_display_obj
681 c.pull_request = pull_request_display_obj
680 c.pull_request_latest = pull_request_latest
682 c.pull_request_latest = pull_request_latest
681
683
682 pr_closed = pull_request_latest.is_closed()
684 pr_closed = pull_request_latest.is_closed()
683 if compare or (at_version and not at_version == 'latest'):
685 if compare or (at_version and not at_version == 'latest'):
684 c.allowed_to_change_status = False
686 c.allowed_to_change_status = False
685 c.allowed_to_update = False
687 c.allowed_to_update = False
686 c.allowed_to_merge = False
688 c.allowed_to_merge = False
687 c.allowed_to_delete = False
689 c.allowed_to_delete = False
688 c.allowed_to_comment = False
690 c.allowed_to_comment = False
691 c.allowed_to_close = False
689 else:
692 else:
690 c.allowed_to_change_status = PullRequestModel(). \
693 c.allowed_to_change_status = PullRequestModel(). \
691 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
694 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
695 and not pr_closed
696
692 c.allowed_to_update = PullRequestModel().check_user_update(
697 c.allowed_to_update = PullRequestModel().check_user_update(
693 pull_request_latest, c.rhodecode_user) and not pr_closed
698 pull_request_latest, c.rhodecode_user) and not pr_closed
694 c.allowed_to_merge = PullRequestModel().check_user_merge(
699 c.allowed_to_merge = PullRequestModel().check_user_merge(
695 pull_request_latest, c.rhodecode_user) and not pr_closed
700 pull_request_latest, c.rhodecode_user) and not pr_closed
696 c.allowed_to_delete = PullRequestModel().check_user_delete(
701 c.allowed_to_delete = PullRequestModel().check_user_delete(
697 pull_request_latest, c.rhodecode_user) and not pr_closed
702 pull_request_latest, c.rhodecode_user) and not pr_closed
698 c.allowed_to_comment = not pr_closed
703 c.allowed_to_comment = not pr_closed
704 c.allowed_to_close = c.allowed_to_change_status and not pr_closed
699
705
700 # check merge capabilities
706 # check merge capabilities
701 _merge_check = MergeCheck.validate(
707 _merge_check = MergeCheck.validate(
702 pull_request_latest, user=c.rhodecode_user)
708 pull_request_latest, user=c.rhodecode_user)
703 c.pr_merge_errors = _merge_check.error_details
709 c.pr_merge_errors = _merge_check.error_details
704 c.pr_merge_possible = not _merge_check.failed
710 c.pr_merge_possible = not _merge_check.failed
705 c.pr_merge_message = _merge_check.merge_msg
711 c.pr_merge_message = _merge_check.merge_msg
706
712
713 c.pull_request_review_status = _merge_check.review_status
707 if merge_checks:
714 if merge_checks:
708 return render('/pullrequests/pullrequest_merge_checks.mako')
715 return render('/pullrequests/pullrequest_merge_checks.mako')
709
716
710 comments_model = CommentsModel()
717 comments_model = CommentsModel()
711
718
712 # reviewers and statuses
719 # reviewers and statuses
713 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
720 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
714 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
721 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
715 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
716
722
717 # GENERAL COMMENTS with versions #
723 # GENERAL COMMENTS with versions #
718 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
724 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
719 q = q.order_by(ChangesetComment.comment_id.asc())
725 q = q.order_by(ChangesetComment.comment_id.asc())
720 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
726 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
721
727
722 # pick comments we want to render at current version
728 # pick comments we want to render at current version
723 c.comment_versions = comments_model.aggregate_comments(
729 c.comment_versions = comments_model.aggregate_comments(
724 general_comments, versions, c.at_version_num)
730 general_comments, versions, c.at_version_num)
725 c.comments = c.comment_versions[c.at_version_num]['until']
731 c.comments = c.comment_versions[c.at_version_num]['until']
726
732
727 # INLINE COMMENTS with versions #
733 # INLINE COMMENTS with versions #
728 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
734 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
729 q = q.order_by(ChangesetComment.comment_id.asc())
735 q = q.order_by(ChangesetComment.comment_id.asc())
730 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
736 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
731 c.inline_versions = comments_model.aggregate_comments(
737 c.inline_versions = comments_model.aggregate_comments(
732 inline_comments, versions, c.at_version_num, inline=True)
738 inline_comments, versions, c.at_version_num, inline=True)
733
739
734 # inject latest version
740 # inject latest version
735 latest_ver = PullRequest.get_pr_display_object(
741 latest_ver = PullRequest.get_pr_display_object(
736 pull_request_latest, pull_request_latest)
742 pull_request_latest, pull_request_latest)
737
743
738 c.versions = versions + [latest_ver]
744 c.versions = versions + [latest_ver]
739
745
740 # if we use version, then do not show later comments
746 # if we use version, then do not show later comments
741 # than current version
747 # than current version
742 display_inline_comments = collections.defaultdict(
748 display_inline_comments = collections.defaultdict(
743 lambda: collections.defaultdict(list))
749 lambda: collections.defaultdict(list))
744 for co in inline_comments:
750 for co in inline_comments:
745 if c.at_version_num:
751 if c.at_version_num:
746 # pick comments that are at least UPTO given version, so we
752 # pick comments that are at least UPTO given version, so we
747 # don't render comments for higher version
753 # don't render comments for higher version
748 should_render = co.pull_request_version_id and \
754 should_render = co.pull_request_version_id and \
749 co.pull_request_version_id <= c.at_version_num
755 co.pull_request_version_id <= c.at_version_num
750 else:
756 else:
751 # showing all, for 'latest'
757 # showing all, for 'latest'
752 should_render = True
758 should_render = True
753
759
754 if should_render:
760 if should_render:
755 display_inline_comments[co.f_path][co.line_no].append(co)
761 display_inline_comments[co.f_path][co.line_no].append(co)
756
762
757 # load diff data into template context, if we use compare mode then
763 # load diff data into template context, if we use compare mode then
758 # diff is calculated based on changes between versions of PR
764 # diff is calculated based on changes between versions of PR
759
765
760 source_repo = pull_request_at_ver.source_repo
766 source_repo = pull_request_at_ver.source_repo
761 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
767 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
762
768
763 target_repo = pull_request_at_ver.target_repo
769 target_repo = pull_request_at_ver.target_repo
764 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
770 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
765
771
766 if compare:
772 if compare:
767 # in compare switch the diff base to latest commit from prev version
773 # in compare switch the diff base to latest commit from prev version
768 target_ref_id = prev_pull_request_display_obj.revisions[0]
774 target_ref_id = prev_pull_request_display_obj.revisions[0]
769
775
770 # despite opening commits for bookmarks/branches/tags, we always
776 # despite opening commits for bookmarks/branches/tags, we always
771 # convert this to rev to prevent changes after bookmark or branch change
777 # convert this to rev to prevent changes after bookmark or branch change
772 c.source_ref_type = 'rev'
778 c.source_ref_type = 'rev'
773 c.source_ref = source_ref_id
779 c.source_ref = source_ref_id
774
780
775 c.target_ref_type = 'rev'
781 c.target_ref_type = 'rev'
776 c.target_ref = target_ref_id
782 c.target_ref = target_ref_id
777
783
778 c.source_repo = source_repo
784 c.source_repo = source_repo
779 c.target_repo = target_repo
785 c.target_repo = target_repo
780
786
781 # diff_limit is the old behavior, will cut off the whole diff
787 # diff_limit is the old behavior, will cut off the whole diff
782 # if the limit is applied otherwise will just hide the
788 # if the limit is applied otherwise will just hide the
783 # big files from the front-end
789 # big files from the front-end
784 diff_limit = self.cut_off_limit_diff
790 diff_limit = self.cut_off_limit_diff
785 file_limit = self.cut_off_limit_file
791 file_limit = self.cut_off_limit_file
786
792
787 c.commit_ranges = []
793 c.commit_ranges = []
788 source_commit = EmptyCommit()
794 source_commit = EmptyCommit()
789 target_commit = EmptyCommit()
795 target_commit = EmptyCommit()
790 c.missing_requirements = False
796 c.missing_requirements = False
791
797
792 # try first shadow repo, fallback to regular repo
798 # try first shadow repo, fallback to regular repo
793 try:
799 try:
794 commits_source_repo = pull_request_latest.get_shadow_repo()
800 commits_source_repo = pull_request_latest.get_shadow_repo()
795 except Exception:
801 except Exception:
796 log.debug('Failed to get shadow repo', exc_info=True)
802 log.debug('Failed to get shadow repo', exc_info=True)
797 commits_source_repo = source_repo.scm_instance()
803 commits_source_repo = source_repo.scm_instance()
798
804
799 c.commits_source_repo = commits_source_repo
805 c.commits_source_repo = commits_source_repo
800 commit_cache = {}
806 commit_cache = {}
801 try:
807 try:
802 pre_load = ["author", "branch", "date", "message"]
808 pre_load = ["author", "branch", "date", "message"]
803 show_revs = pull_request_at_ver.revisions
809 show_revs = pull_request_at_ver.revisions
804 for rev in show_revs:
810 for rev in show_revs:
805 comm = commits_source_repo.get_commit(
811 comm = commits_source_repo.get_commit(
806 commit_id=rev, pre_load=pre_load)
812 commit_id=rev, pre_load=pre_load)
807 c.commit_ranges.append(comm)
813 c.commit_ranges.append(comm)
808 commit_cache[comm.raw_id] = comm
814 commit_cache[comm.raw_id] = comm
809
815
810 target_commit = commits_source_repo.get_commit(
816 target_commit = commits_source_repo.get_commit(
811 commit_id=safe_str(target_ref_id))
817 commit_id=safe_str(target_ref_id))
812 source_commit = commits_source_repo.get_commit(
818 source_commit = commits_source_repo.get_commit(
813 commit_id=safe_str(source_ref_id))
819 commit_id=safe_str(source_ref_id))
814 except CommitDoesNotExistError:
820 except CommitDoesNotExistError:
815 pass
821 pass
816 except RepositoryRequirementError:
822 except RepositoryRequirementError:
817 log.warning(
823 log.warning(
818 'Failed to get all required data from repo', exc_info=True)
824 'Failed to get all required data from repo', exc_info=True)
819 c.missing_requirements = True
825 c.missing_requirements = True
820
826
821 c.statuses = source_repo.statuses(
827 c.statuses = source_repo.statuses(
822 [x.raw_id for x in c.commit_ranges])
828 [x.raw_id for x in c.commit_ranges])
823
829
824 # auto collapse if we have more than limit
830 # auto collapse if we have more than limit
825 collapse_limit = diffs.DiffProcessor._collapse_commits_over
831 collapse_limit = diffs.DiffProcessor._collapse_commits_over
826 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
832 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
827 c.compare_mode = compare
833 c.compare_mode = compare
828
834
829 c.missing_commits = False
835 c.missing_commits = False
830 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
836 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
831 or source_commit == target_commit):
837 or source_commit == target_commit):
832
838
833 c.missing_commits = True
839 c.missing_commits = True
834 else:
840 else:
835
841
836 c.diffset = self._get_diffset(
842 c.diffset = self._get_diffset(
837 commits_source_repo, source_ref_id, target_ref_id,
843 commits_source_repo, source_ref_id, target_ref_id,
838 target_commit, source_commit,
844 target_commit, source_commit,
839 diff_limit, file_limit, display_inline_comments)
845 diff_limit, file_limit, display_inline_comments)
840
846
841 c.limited_diff = c.diffset.limited_diff
847 c.limited_diff = c.diffset.limited_diff
842
848
843 # calculate removed files that are bound to comments
849 # calculate removed files that are bound to comments
844 comment_deleted_files = [
850 comment_deleted_files = [
845 fname for fname in display_inline_comments
851 fname for fname in display_inline_comments
846 if fname not in c.diffset.file_stats]
852 if fname not in c.diffset.file_stats]
847
853
848 c.deleted_files_comments = collections.defaultdict(dict)
854 c.deleted_files_comments = collections.defaultdict(dict)
849 for fname, per_line_comments in display_inline_comments.items():
855 for fname, per_line_comments in display_inline_comments.items():
850 if fname in comment_deleted_files:
856 if fname in comment_deleted_files:
851 c.deleted_files_comments[fname]['stats'] = 0
857 c.deleted_files_comments[fname]['stats'] = 0
852 c.deleted_files_comments[fname]['comments'] = list()
858 c.deleted_files_comments[fname]['comments'] = list()
853 for lno, comments in per_line_comments.items():
859 for lno, comments in per_line_comments.items():
854 c.deleted_files_comments[fname]['comments'].extend(
860 c.deleted_files_comments[fname]['comments'].extend(
855 comments)
861 comments)
856
862
857 # this is a hack to properly display links, when creating PR, the
863 # this is a hack to properly display links, when creating PR, the
858 # compare view and others uses different notation, and
864 # compare view and others uses different notation, and
859 # compare_commits.mako renders links based on the target_repo.
865 # compare_commits.mako renders links based on the target_repo.
860 # We need to swap that here to generate it properly on the html side
866 # We need to swap that here to generate it properly on the html side
861 c.target_repo = c.source_repo
867 c.target_repo = c.source_repo
862
868
863 if c.allowed_to_update:
869 c.commit_statuses = ChangesetStatus.STATUSES
864 force_close = ('forced_closed', _('Close Pull Request'))
865 statuses = ChangesetStatus.STATUSES + [force_close]
866 else:
867 statuses = ChangesetStatus.STATUSES
868 c.commit_statuses = statuses
869
870
870 c.show_version_changes = not pr_closed
871 c.show_version_changes = not pr_closed
871 if c.show_version_changes:
872 if c.show_version_changes:
872 cur_obj = pull_request_at_ver
873 cur_obj = pull_request_at_ver
873 prev_obj = prev_pull_request_at_ver
874 prev_obj = prev_pull_request_at_ver
874
875
875 old_commit_ids = prev_obj.revisions
876 old_commit_ids = prev_obj.revisions
876 new_commit_ids = cur_obj.revisions
877 new_commit_ids = cur_obj.revisions
877 commit_changes = PullRequestModel()._calculate_commit_id_changes(
878 commit_changes = PullRequestModel()._calculate_commit_id_changes(
878 old_commit_ids, new_commit_ids)
879 old_commit_ids, new_commit_ids)
879 c.commit_changes_summary = commit_changes
880 c.commit_changes_summary = commit_changes
880
881
881 # calculate the diff for commits between versions
882 # calculate the diff for commits between versions
882 c.commit_changes = []
883 c.commit_changes = []
883 mark = lambda cs, fw: list(
884 mark = lambda cs, fw: list(
884 h.itertools.izip_longest([], cs, fillvalue=fw))
885 h.itertools.izip_longest([], cs, fillvalue=fw))
885 for c_type, raw_id in mark(commit_changes.added, 'a') \
886 for c_type, raw_id in mark(commit_changes.added, 'a') \
886 + mark(commit_changes.removed, 'r') \
887 + mark(commit_changes.removed, 'r') \
887 + mark(commit_changes.common, 'c'):
888 + mark(commit_changes.common, 'c'):
888
889
889 if raw_id in commit_cache:
890 if raw_id in commit_cache:
890 commit = commit_cache[raw_id]
891 commit = commit_cache[raw_id]
891 else:
892 else:
892 try:
893 try:
893 commit = commits_source_repo.get_commit(raw_id)
894 commit = commits_source_repo.get_commit(raw_id)
894 except CommitDoesNotExistError:
895 except CommitDoesNotExistError:
895 # in case we fail extracting still use "dummy" commit
896 # in case we fail extracting still use "dummy" commit
896 # for display in commit diff
897 # for display in commit diff
897 commit = h.AttributeDict(
898 commit = h.AttributeDict(
898 {'raw_id': raw_id,
899 {'raw_id': raw_id,
899 'message': 'EMPTY or MISSING COMMIT'})
900 'message': 'EMPTY or MISSING COMMIT'})
900 c.commit_changes.append([c_type, commit])
901 c.commit_changes.append([c_type, commit])
901
902
902 # current user review statuses for each version
903 # current user review statuses for each version
903 c.review_versions = {}
904 c.review_versions = {}
904 if c.rhodecode_user.user_id in allowed_reviewers:
905 if c.rhodecode_user.user_id in allowed_reviewers:
905 for co in general_comments:
906 for co in general_comments:
906 if co.author.user_id == c.rhodecode_user.user_id:
907 if co.author.user_id == c.rhodecode_user.user_id:
907 # each comment has a status change
908 # each comment has a status change
908 status = co.status_change
909 status = co.status_change
909 if status:
910 if status:
910 _ver_pr = status[0].comment.pull_request_version_id
911 _ver_pr = status[0].comment.pull_request_version_id
911 c.review_versions[_ver_pr] = status[0]
912 c.review_versions[_ver_pr] = status[0]
912
913
913 return render('/pullrequests/pullrequest_show.mako')
914 return render('/pullrequests/pullrequest_show.mako')
914
915
915 @LoginRequired()
916 @LoginRequired()
916 @NotAnonymous()
917 @NotAnonymous()
917 @HasRepoPermissionAnyDecorator(
918 @HasRepoPermissionAnyDecorator(
918 'repository.read', 'repository.write', 'repository.admin')
919 'repository.read', 'repository.write', 'repository.admin')
919 @auth.CSRFRequired()
920 @auth.CSRFRequired()
920 @jsonify
921 @jsonify
921 def comment(self, repo_name, pull_request_id):
922 def comment(self, repo_name, pull_request_id):
922 pull_request_id = safe_int(pull_request_id)
923 pull_request_id = safe_int(pull_request_id)
923 pull_request = PullRequest.get_or_404(pull_request_id)
924 pull_request = PullRequest.get_or_404(pull_request_id)
924 if pull_request.is_closed():
925 if pull_request.is_closed():
925 raise HTTPForbidden()
926 raise HTTPForbidden()
926
927
927 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
928 # as a changeset status, still we want to send it in one value.
929 status = request.POST.get('changeset_status', None)
928 status = request.POST.get('changeset_status', None)
930 text = request.POST.get('text')
929 text = request.POST.get('text')
931 comment_type = request.POST.get('comment_type')
930 comment_type = request.POST.get('comment_type')
932 resolves_comment_id = request.POST.get('resolves_comment_id', None)
931 resolves_comment_id = request.POST.get('resolves_comment_id', None)
932 close_pull_request = request.POST.get('close_pull_request')
933
933
934 if status and '_closed' in status:
934 close_pr = False
935 if close_pull_request:
935 close_pr = True
936 close_pr = True
936 status = status.replace('_closed', '')
937 pull_request_review_status = pull_request.calculated_review_status()
937 else:
938 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
938 close_pr = False
939 # approved only if we have voting consent
939
940 status = ChangesetStatus.STATUS_APPROVED
940 forced = (status == 'forced')
941 else:
941 if forced:
942 status = ChangesetStatus.STATUS_REJECTED
942 status = 'rejected'
943
943
944 allowed_to_change_status = PullRequestModel().check_user_change_status(
944 allowed_to_change_status = PullRequestModel().check_user_change_status(
945 pull_request, c.rhodecode_user)
945 pull_request, c.rhodecode_user)
946
946
947 if status and allowed_to_change_status:
947 if status and allowed_to_change_status:
948 message = (_('Status change %(transition_icon)s %(status)s')
948 message = (_('Status change %(transition_icon)s %(status)s')
949 % {'transition_icon': '>',
949 % {'transition_icon': '>',
950 'status': ChangesetStatus.get_status_lbl(status)})
950 'status': ChangesetStatus.get_status_lbl(status)})
951 if close_pr:
951 if close_pr:
952 message = _('Closing with') + ' ' + message
952 message = _('Closing with') + ' ' + message
953 text = text or message
953 text = text or message
954 comm = CommentsModel().create(
954 comm = CommentsModel().create(
955 text=text,
955 text=text,
956 repo=c.rhodecode_db_repo.repo_id,
956 repo=c.rhodecode_db_repo.repo_id,
957 user=c.rhodecode_user.user_id,
957 user=c.rhodecode_user.user_id,
958 pull_request=pull_request_id,
958 pull_request=pull_request_id,
959 f_path=request.POST.get('f_path'),
959 f_path=request.POST.get('f_path'),
960 line_no=request.POST.get('line'),
960 line_no=request.POST.get('line'),
961 status_change=(ChangesetStatus.get_status_lbl(status)
961 status_change=(ChangesetStatus.get_status_lbl(status)
962 if status and allowed_to_change_status else None),
962 if status and allowed_to_change_status else None),
963 status_change_type=(status
963 status_change_type=(status
964 if status and allowed_to_change_status else None),
964 if status and allowed_to_change_status else None),
965 closing_pr=close_pr,
965 closing_pr=close_pr,
966 comment_type=comment_type,
966 comment_type=comment_type,
967 resolves_comment_id=resolves_comment_id
967 resolves_comment_id=resolves_comment_id
968 )
968 )
969
969
970 if allowed_to_change_status:
970 if allowed_to_change_status:
971 old_calculated_status = pull_request.calculated_review_status()
971 old_calculated_status = pull_request.calculated_review_status()
972 # get status if set !
972 # get status if set !
973 if status:
973 if status:
974 ChangesetStatusModel().set_status(
974 ChangesetStatusModel().set_status(
975 c.rhodecode_db_repo.repo_id,
975 c.rhodecode_db_repo.repo_id,
976 status,
976 status,
977 c.rhodecode_user.user_id,
977 c.rhodecode_user.user_id,
978 comm,
978 comm,
979 pull_request=pull_request_id
979 pull_request=pull_request_id
980 )
980 )
981
981
982 Session().flush()
982 Session().flush()
983 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
983 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
984 # we now calculate the status of pull request, and based on that
984 # we now calculate the status of pull request, and based on that
985 # calculation we set the commits status
985 # calculation we set the commits status
986 calculated_status = pull_request.calculated_review_status()
986 calculated_status = pull_request.calculated_review_status()
987 if old_calculated_status != calculated_status:
987 if old_calculated_status != calculated_status:
988 PullRequestModel()._trigger_pull_request_hook(
988 PullRequestModel()._trigger_pull_request_hook(
989 pull_request, c.rhodecode_user, 'review_status_change')
989 pull_request, c.rhodecode_user, 'review_status_change')
990
990
991 calculated_status_lbl = ChangesetStatus.get_status_lbl(
991 calculated_status_lbl = ChangesetStatus.get_status_lbl(
992 calculated_status)
992 calculated_status)
993
993
994 if close_pr:
994 if close_pr:
995 status_completed = (
995 status_completed = (
996 calculated_status in [ChangesetStatus.STATUS_APPROVED,
996 calculated_status in [ChangesetStatus.STATUS_APPROVED,
997 ChangesetStatus.STATUS_REJECTED])
997 ChangesetStatus.STATUS_REJECTED])
998 if forced or status_completed:
998 if close_pull_request or status_completed:
999 PullRequestModel().close_pull_request(
999 PullRequestModel().close_pull_request(
1000 pull_request_id, c.rhodecode_user)
1000 pull_request_id, c.rhodecode_user)
1001 else:
1001 else:
1002 h.flash(_('Closing pull request on other statuses than '
1002 h.flash(_('Closing pull request on other statuses than '
1003 'rejected or approved is forbidden. '
1003 'rejected or approved is forbidden. '
1004 'Calculated status from all reviewers '
1004 'Calculated status from all reviewers '
1005 'is currently: %s') % calculated_status_lbl,
1005 'is currently: %s') % calculated_status_lbl,
1006 category='warning')
1006 category='warning')
1007
1007
1008 Session().commit()
1008 Session().commit()
1009
1009
1010 if not request.is_xhr:
1010 if not request.is_xhr:
1011 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1011 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1012 pull_request_id=pull_request_id))
1012 pull_request_id=pull_request_id))
1013
1013
1014 data = {
1014 data = {
1015 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1015 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1016 }
1016 }
1017 if comm:
1017 if comm:
1018 c.co = comm
1018 c.co = comm
1019 c.inline_comment = True if comm.line_no else False
1019 c.inline_comment = True if comm.line_no else False
1020 data.update(comm.get_dict())
1020 data.update(comm.get_dict())
1021 data.update({'rendered_text':
1021 data.update({'rendered_text':
1022 render('changeset/changeset_comment_block.mako')})
1022 render('changeset/changeset_comment_block.mako')})
1023
1023
1024 return data
1024 return data
1025
1025
1026 @LoginRequired()
1026 @LoginRequired()
1027 @NotAnonymous()
1027 @NotAnonymous()
1028 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1028 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1029 'repository.admin')
1029 'repository.admin')
1030 @auth.CSRFRequired()
1030 @auth.CSRFRequired()
1031 @jsonify
1031 @jsonify
1032 def delete_comment(self, repo_name, comment_id):
1032 def delete_comment(self, repo_name, comment_id):
1033 return self._delete_comment(comment_id)
1033 return self._delete_comment(comment_id)
1034
1034
1035 def _delete_comment(self, comment_id):
1035 def _delete_comment(self, comment_id):
1036 comment_id = safe_int(comment_id)
1036 comment_id = safe_int(comment_id)
1037 co = ChangesetComment.get_or_404(comment_id)
1037 co = ChangesetComment.get_or_404(comment_id)
1038 if co.pull_request.is_closed():
1038 if co.pull_request.is_closed():
1039 # don't allow deleting comments on closed pull request
1039 # don't allow deleting comments on closed pull request
1040 raise HTTPForbidden()
1040 raise HTTPForbidden()
1041
1041
1042 is_owner = co.author.user_id == c.rhodecode_user.user_id
1042 is_owner = co.author.user_id == c.rhodecode_user.user_id
1043 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1043 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1044 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1044 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1045 old_calculated_status = co.pull_request.calculated_review_status()
1045 old_calculated_status = co.pull_request.calculated_review_status()
1046 CommentsModel().delete(comment=co)
1046 CommentsModel().delete(comment=co)
1047 Session().commit()
1047 Session().commit()
1048 calculated_status = co.pull_request.calculated_review_status()
1048 calculated_status = co.pull_request.calculated_review_status()
1049 if old_calculated_status != calculated_status:
1049 if old_calculated_status != calculated_status:
1050 PullRequestModel()._trigger_pull_request_hook(
1050 PullRequestModel()._trigger_pull_request_hook(
1051 co.pull_request, c.rhodecode_user, 'review_status_change')
1051 co.pull_request, c.rhodecode_user, 'review_status_change')
1052 return True
1052 return True
1053 else:
1053 else:
1054 raise HTTPForbidden()
1054 raise HTTPForbidden()
@@ -1,1420 +1,1423 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from sqlalchemy import or_
34 from sqlalchemy import or_
35
35
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
39 from rhodecode.lib.markup_renderer import (
39 from rhodecode.lib.markup_renderer import (
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
41 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils import action_logger
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 CommitDoesNotExistError, EmptyRepositoryError)
47 CommitDoesNotExistError, EmptyRepositoryError)
48 from rhodecode.model import BaseModel
48 from rhodecode.model import BaseModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.db import (
51 from rhodecode.model.db import (
52 PullRequest, PullRequestReviewers, ChangesetStatus,
52 PullRequest, PullRequestReviewers, ChangesetStatus,
53 PullRequestVersion, ChangesetComment, Repository)
53 PullRequestVersion, ChangesetComment, Repository)
54 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
55 from rhodecode.model.notification import NotificationModel, \
55 from rhodecode.model.notification import NotificationModel, \
56 EmailNotificationModel
56 EmailNotificationModel
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.settings import VcsSettingsModel
58 from rhodecode.model.settings import VcsSettingsModel
59
59
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 # Data structure to hold the response data when updating commits during a pull
64 # Data structure to hold the response data when updating commits during a pull
65 # request update.
65 # request update.
66 UpdateResponse = namedtuple(
66 UpdateResponse = namedtuple(
67 'UpdateResponse', 'executed, reason, new, old, changes')
67 'UpdateResponse', 'executed, reason, new, old, changes')
68
68
69
69
70 class PullRequestModel(BaseModel):
70 class PullRequestModel(BaseModel):
71
71
72 cls = PullRequest
72 cls = PullRequest
73
73
74 DIFF_CONTEXT = 3
74 DIFF_CONTEXT = 3
75
75
76 MERGE_STATUS_MESSAGES = {
76 MERGE_STATUS_MESSAGES = {
77 MergeFailureReason.NONE: lazy_ugettext(
77 MergeFailureReason.NONE: lazy_ugettext(
78 'This pull request can be automatically merged.'),
78 'This pull request can be automatically merged.'),
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
80 'This pull request cannot be merged because of an unhandled'
80 'This pull request cannot be merged because of an unhandled'
81 ' exception.'),
81 ' exception.'),
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
83 'This pull request cannot be merged because of merge conflicts.'),
83 'This pull request cannot be merged because of merge conflicts.'),
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
85 'This pull request could not be merged because push to target'
85 'This pull request could not be merged because push to target'
86 ' failed.'),
86 ' failed.'),
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
88 'This pull request cannot be merged because the target is not a'
88 'This pull request cannot be merged because the target is not a'
89 ' head.'),
89 ' head.'),
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
91 'This pull request cannot be merged because the source contains'
91 'This pull request cannot be merged because the source contains'
92 ' more branches than the target.'),
92 ' more branches than the target.'),
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
94 'This pull request cannot be merged because the target has'
94 'This pull request cannot be merged because the target has'
95 ' multiple heads.'),
95 ' multiple heads.'),
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
97 'This pull request cannot be merged because the target repository'
97 'This pull request cannot be merged because the target repository'
98 ' is locked.'),
98 ' is locked.'),
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
100 'This pull request cannot be merged because the target or the '
100 'This pull request cannot be merged because the target or the '
101 'source reference is missing.'),
101 'source reference is missing.'),
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
103 'This pull request cannot be merged because the target '
103 'This pull request cannot be merged because the target '
104 'reference is missing.'),
104 'reference is missing.'),
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
106 'This pull request cannot be merged because the source '
106 'This pull request cannot be merged because the source '
107 'reference is missing.'),
107 'reference is missing.'),
108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
109 'This pull request cannot be merged because of conflicts related '
109 'This pull request cannot be merged because of conflicts related '
110 'to sub repositories.'),
110 'to sub repositories.'),
111 }
111 }
112
112
113 UPDATE_STATUS_MESSAGES = {
113 UPDATE_STATUS_MESSAGES = {
114 UpdateFailureReason.NONE: lazy_ugettext(
114 UpdateFailureReason.NONE: lazy_ugettext(
115 'Pull request update successful.'),
115 'Pull request update successful.'),
116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
117 'Pull request update failed because of an unknown error.'),
117 'Pull request update failed because of an unknown error.'),
118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
119 'No update needed because the source reference is already '
119 'No update needed because the source reference is already '
120 'up to date.'),
120 'up to date.'),
121 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
121 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
122 'Pull request cannot be updated because the reference type is '
122 'Pull request cannot be updated because the reference type is '
123 'not supported for an update.'),
123 'not supported for an update.'),
124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 'This pull request cannot be updated because the target '
125 'This pull request cannot be updated because the target '
126 'reference is missing.'),
126 'reference is missing.'),
127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 'This pull request cannot be updated because the source '
128 'This pull request cannot be updated because the source '
129 'reference is missing.'),
129 'reference is missing.'),
130 }
130 }
131
131
132 def __get_pull_request(self, pull_request):
132 def __get_pull_request(self, pull_request):
133 return self._get_instance((
133 return self._get_instance((
134 PullRequest, PullRequestVersion), pull_request)
134 PullRequest, PullRequestVersion), pull_request)
135
135
136 def _check_perms(self, perms, pull_request, user, api=False):
136 def _check_perms(self, perms, pull_request, user, api=False):
137 if not api:
137 if not api:
138 return h.HasRepoPermissionAny(*perms)(
138 return h.HasRepoPermissionAny(*perms)(
139 user=user, repo_name=pull_request.target_repo.repo_name)
139 user=user, repo_name=pull_request.target_repo.repo_name)
140 else:
140 else:
141 return h.HasRepoPermissionAnyApi(*perms)(
141 return h.HasRepoPermissionAnyApi(*perms)(
142 user=user, repo_name=pull_request.target_repo.repo_name)
142 user=user, repo_name=pull_request.target_repo.repo_name)
143
143
144 def check_user_read(self, pull_request, user, api=False):
144 def check_user_read(self, pull_request, user, api=False):
145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 return self._check_perms(_perms, pull_request, user, api)
146 return self._check_perms(_perms, pull_request, user, api)
147
147
148 def check_user_merge(self, pull_request, user, api=False):
148 def check_user_merge(self, pull_request, user, api=False):
149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 return self._check_perms(_perms, pull_request, user, api)
150 return self._check_perms(_perms, pull_request, user, api)
151
151
152 def check_user_update(self, pull_request, user, api=False):
152 def check_user_update(self, pull_request, user, api=False):
153 owner = user.user_id == pull_request.user_id
153 owner = user.user_id == pull_request.user_id
154 return self.check_user_merge(pull_request, user, api) or owner
154 return self.check_user_merge(pull_request, user, api) or owner
155
155
156 def check_user_delete(self, pull_request, user):
156 def check_user_delete(self, pull_request, user):
157 owner = user.user_id == pull_request.user_id
157 owner = user.user_id == pull_request.user_id
158 _perms = ('repository.admin',)
158 _perms = ('repository.admin',)
159 return self._check_perms(_perms, pull_request, user) or owner
159 return self._check_perms(_perms, pull_request, user) or owner
160
160
161 def check_user_change_status(self, pull_request, user, api=False):
161 def check_user_change_status(self, pull_request, user, api=False):
162 reviewer = user.user_id in [x.user_id for x in
162 reviewer = user.user_id in [x.user_id for x in
163 pull_request.reviewers]
163 pull_request.reviewers]
164 return self.check_user_update(pull_request, user, api) or reviewer
164 return self.check_user_update(pull_request, user, api) or reviewer
165
165
166 def get(self, pull_request):
166 def get(self, pull_request):
167 return self.__get_pull_request(pull_request)
167 return self.__get_pull_request(pull_request)
168
168
169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
170 opened_by=None, order_by=None,
170 opened_by=None, order_by=None,
171 order_dir='desc'):
171 order_dir='desc'):
172 repo = None
172 repo = None
173 if repo_name:
173 if repo_name:
174 repo = self._get_repo(repo_name)
174 repo = self._get_repo(repo_name)
175
175
176 q = PullRequest.query()
176 q = PullRequest.query()
177
177
178 # source or target
178 # source or target
179 if repo and source:
179 if repo and source:
180 q = q.filter(PullRequest.source_repo == repo)
180 q = q.filter(PullRequest.source_repo == repo)
181 elif repo:
181 elif repo:
182 q = q.filter(PullRequest.target_repo == repo)
182 q = q.filter(PullRequest.target_repo == repo)
183
183
184 # closed,opened
184 # closed,opened
185 if statuses:
185 if statuses:
186 q = q.filter(PullRequest.status.in_(statuses))
186 q = q.filter(PullRequest.status.in_(statuses))
187
187
188 # opened by filter
188 # opened by filter
189 if opened_by:
189 if opened_by:
190 q = q.filter(PullRequest.user_id.in_(opened_by))
190 q = q.filter(PullRequest.user_id.in_(opened_by))
191
191
192 if order_by:
192 if order_by:
193 order_map = {
193 order_map = {
194 'name_raw': PullRequest.pull_request_id,
194 'name_raw': PullRequest.pull_request_id,
195 'title': PullRequest.title,
195 'title': PullRequest.title,
196 'updated_on_raw': PullRequest.updated_on,
196 'updated_on_raw': PullRequest.updated_on,
197 'target_repo': PullRequest.target_repo_id
197 'target_repo': PullRequest.target_repo_id
198 }
198 }
199 if order_dir == 'asc':
199 if order_dir == 'asc':
200 q = q.order_by(order_map[order_by].asc())
200 q = q.order_by(order_map[order_by].asc())
201 else:
201 else:
202 q = q.order_by(order_map[order_by].desc())
202 q = q.order_by(order_map[order_by].desc())
203
203
204 return q
204 return q
205
205
206 def count_all(self, repo_name, source=False, statuses=None,
206 def count_all(self, repo_name, source=False, statuses=None,
207 opened_by=None):
207 opened_by=None):
208 """
208 """
209 Count the number of pull requests for a specific repository.
209 Count the number of pull requests for a specific repository.
210
210
211 :param repo_name: target or source repo
211 :param repo_name: target or source repo
212 :param source: boolean flag to specify if repo_name refers to source
212 :param source: boolean flag to specify if repo_name refers to source
213 :param statuses: list of pull request statuses
213 :param statuses: list of pull request statuses
214 :param opened_by: author user of the pull request
214 :param opened_by: author user of the pull request
215 :returns: int number of pull requests
215 :returns: int number of pull requests
216 """
216 """
217 q = self._prepare_get_all_query(
217 q = self._prepare_get_all_query(
218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
219
219
220 return q.count()
220 return q.count()
221
221
222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
223 offset=0, length=None, order_by=None, order_dir='desc'):
223 offset=0, length=None, order_by=None, order_dir='desc'):
224 """
224 """
225 Get all pull requests for a specific repository.
225 Get all pull requests for a specific repository.
226
226
227 :param repo_name: target or source repo
227 :param repo_name: target or source repo
228 :param source: boolean flag to specify if repo_name refers to source
228 :param source: boolean flag to specify if repo_name refers to source
229 :param statuses: list of pull request statuses
229 :param statuses: list of pull request statuses
230 :param opened_by: author user of the pull request
230 :param opened_by: author user of the pull request
231 :param offset: pagination offset
231 :param offset: pagination offset
232 :param length: length of returned list
232 :param length: length of returned list
233 :param order_by: order of the returned list
233 :param order_by: order of the returned list
234 :param order_dir: 'asc' or 'desc' ordering direction
234 :param order_dir: 'asc' or 'desc' ordering direction
235 :returns: list of pull requests
235 :returns: list of pull requests
236 """
236 """
237 q = self._prepare_get_all_query(
237 q = self._prepare_get_all_query(
238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
239 order_by=order_by, order_dir=order_dir)
239 order_by=order_by, order_dir=order_dir)
240
240
241 if length:
241 if length:
242 pull_requests = q.limit(length).offset(offset).all()
242 pull_requests = q.limit(length).offset(offset).all()
243 else:
243 else:
244 pull_requests = q.all()
244 pull_requests = q.all()
245
245
246 return pull_requests
246 return pull_requests
247
247
248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
249 opened_by=None):
249 opened_by=None):
250 """
250 """
251 Count the number of pull requests for a specific repository that are
251 Count the number of pull requests for a specific repository that are
252 awaiting review.
252 awaiting review.
253
253
254 :param repo_name: target or source repo
254 :param repo_name: target or source repo
255 :param source: boolean flag to specify if repo_name refers to source
255 :param source: boolean flag to specify if repo_name refers to source
256 :param statuses: list of pull request statuses
256 :param statuses: list of pull request statuses
257 :param opened_by: author user of the pull request
257 :param opened_by: author user of the pull request
258 :returns: int number of pull requests
258 :returns: int number of pull requests
259 """
259 """
260 pull_requests = self.get_awaiting_review(
260 pull_requests = self.get_awaiting_review(
261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
262
262
263 return len(pull_requests)
263 return len(pull_requests)
264
264
265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
266 opened_by=None, offset=0, length=None,
266 opened_by=None, offset=0, length=None,
267 order_by=None, order_dir='desc'):
267 order_by=None, order_dir='desc'):
268 """
268 """
269 Get all pull requests for a specific repository that are awaiting
269 Get all pull requests for a specific repository that are awaiting
270 review.
270 review.
271
271
272 :param repo_name: target or source repo
272 :param repo_name: target or source repo
273 :param source: boolean flag to specify if repo_name refers to source
273 :param source: boolean flag to specify if repo_name refers to source
274 :param statuses: list of pull request statuses
274 :param statuses: list of pull request statuses
275 :param opened_by: author user of the pull request
275 :param opened_by: author user of the pull request
276 :param offset: pagination offset
276 :param offset: pagination offset
277 :param length: length of returned list
277 :param length: length of returned list
278 :param order_by: order of the returned list
278 :param order_by: order of the returned list
279 :param order_dir: 'asc' or 'desc' ordering direction
279 :param order_dir: 'asc' or 'desc' ordering direction
280 :returns: list of pull requests
280 :returns: list of pull requests
281 """
281 """
282 pull_requests = self.get_all(
282 pull_requests = self.get_all(
283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
284 order_by=order_by, order_dir=order_dir)
284 order_by=order_by, order_dir=order_dir)
285
285
286 _filtered_pull_requests = []
286 _filtered_pull_requests = []
287 for pr in pull_requests:
287 for pr in pull_requests:
288 status = pr.calculated_review_status()
288 status = pr.calculated_review_status()
289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
290 ChangesetStatus.STATUS_UNDER_REVIEW]:
290 ChangesetStatus.STATUS_UNDER_REVIEW]:
291 _filtered_pull_requests.append(pr)
291 _filtered_pull_requests.append(pr)
292 if length:
292 if length:
293 return _filtered_pull_requests[offset:offset+length]
293 return _filtered_pull_requests[offset:offset+length]
294 else:
294 else:
295 return _filtered_pull_requests
295 return _filtered_pull_requests
296
296
297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
298 opened_by=None, user_id=None):
298 opened_by=None, user_id=None):
299 """
299 """
300 Count the number of pull requests for a specific repository that are
300 Count the number of pull requests for a specific repository that are
301 awaiting review from a specific user.
301 awaiting review from a specific user.
302
302
303 :param repo_name: target or source repo
303 :param repo_name: target or source repo
304 :param source: boolean flag to specify if repo_name refers to source
304 :param source: boolean flag to specify if repo_name refers to source
305 :param statuses: list of pull request statuses
305 :param statuses: list of pull request statuses
306 :param opened_by: author user of the pull request
306 :param opened_by: author user of the pull request
307 :param user_id: reviewer user of the pull request
307 :param user_id: reviewer user of the pull request
308 :returns: int number of pull requests
308 :returns: int number of pull requests
309 """
309 """
310 pull_requests = self.get_awaiting_my_review(
310 pull_requests = self.get_awaiting_my_review(
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 user_id=user_id)
312 user_id=user_id)
313
313
314 return len(pull_requests)
314 return len(pull_requests)
315
315
316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
317 opened_by=None, user_id=None, offset=0,
317 opened_by=None, user_id=None, offset=0,
318 length=None, order_by=None, order_dir='desc'):
318 length=None, order_by=None, order_dir='desc'):
319 """
319 """
320 Get all pull requests for a specific repository that are awaiting
320 Get all pull requests for a specific repository that are awaiting
321 review from a specific user.
321 review from a specific user.
322
322
323 :param repo_name: target or source repo
323 :param repo_name: target or source repo
324 :param source: boolean flag to specify if repo_name refers to source
324 :param source: boolean flag to specify if repo_name refers to source
325 :param statuses: list of pull request statuses
325 :param statuses: list of pull request statuses
326 :param opened_by: author user of the pull request
326 :param opened_by: author user of the pull request
327 :param user_id: reviewer user of the pull request
327 :param user_id: reviewer user of the pull request
328 :param offset: pagination offset
328 :param offset: pagination offset
329 :param length: length of returned list
329 :param length: length of returned list
330 :param order_by: order of the returned list
330 :param order_by: order of the returned list
331 :param order_dir: 'asc' or 'desc' ordering direction
331 :param order_dir: 'asc' or 'desc' ordering direction
332 :returns: list of pull requests
332 :returns: list of pull requests
333 """
333 """
334 pull_requests = self.get_all(
334 pull_requests = self.get_all(
335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
336 order_by=order_by, order_dir=order_dir)
336 order_by=order_by, order_dir=order_dir)
337
337
338 _my = PullRequestModel().get_not_reviewed(user_id)
338 _my = PullRequestModel().get_not_reviewed(user_id)
339 my_participation = []
339 my_participation = []
340 for pr in pull_requests:
340 for pr in pull_requests:
341 if pr in _my:
341 if pr in _my:
342 my_participation.append(pr)
342 my_participation.append(pr)
343 _filtered_pull_requests = my_participation
343 _filtered_pull_requests = my_participation
344 if length:
344 if length:
345 return _filtered_pull_requests[offset:offset+length]
345 return _filtered_pull_requests[offset:offset+length]
346 else:
346 else:
347 return _filtered_pull_requests
347 return _filtered_pull_requests
348
348
349 def get_not_reviewed(self, user_id):
349 def get_not_reviewed(self, user_id):
350 return [
350 return [
351 x.pull_request for x in PullRequestReviewers.query().filter(
351 x.pull_request for x in PullRequestReviewers.query().filter(
352 PullRequestReviewers.user_id == user_id).all()
352 PullRequestReviewers.user_id == user_id).all()
353 ]
353 ]
354
354
355 def _prepare_participating_query(self, user_id=None, statuses=None,
355 def _prepare_participating_query(self, user_id=None, statuses=None,
356 order_by=None, order_dir='desc'):
356 order_by=None, order_dir='desc'):
357 q = PullRequest.query()
357 q = PullRequest.query()
358 if user_id:
358 if user_id:
359 reviewers_subquery = Session().query(
359 reviewers_subquery = Session().query(
360 PullRequestReviewers.pull_request_id).filter(
360 PullRequestReviewers.pull_request_id).filter(
361 PullRequestReviewers.user_id == user_id).subquery()
361 PullRequestReviewers.user_id == user_id).subquery()
362 user_filter= or_(
362 user_filter= or_(
363 PullRequest.user_id == user_id,
363 PullRequest.user_id == user_id,
364 PullRequest.pull_request_id.in_(reviewers_subquery)
364 PullRequest.pull_request_id.in_(reviewers_subquery)
365 )
365 )
366 q = PullRequest.query().filter(user_filter)
366 q = PullRequest.query().filter(user_filter)
367
367
368 # closed,opened
368 # closed,opened
369 if statuses:
369 if statuses:
370 q = q.filter(PullRequest.status.in_(statuses))
370 q = q.filter(PullRequest.status.in_(statuses))
371
371
372 if order_by:
372 if order_by:
373 order_map = {
373 order_map = {
374 'name_raw': PullRequest.pull_request_id,
374 'name_raw': PullRequest.pull_request_id,
375 'title': PullRequest.title,
375 'title': PullRequest.title,
376 'updated_on_raw': PullRequest.updated_on,
376 'updated_on_raw': PullRequest.updated_on,
377 'target_repo': PullRequest.target_repo_id
377 'target_repo': PullRequest.target_repo_id
378 }
378 }
379 if order_dir == 'asc':
379 if order_dir == 'asc':
380 q = q.order_by(order_map[order_by].asc())
380 q = q.order_by(order_map[order_by].asc())
381 else:
381 else:
382 q = q.order_by(order_map[order_by].desc())
382 q = q.order_by(order_map[order_by].desc())
383
383
384 return q
384 return q
385
385
386 def count_im_participating_in(self, user_id=None, statuses=None):
386 def count_im_participating_in(self, user_id=None, statuses=None):
387 q = self._prepare_participating_query(user_id, statuses=statuses)
387 q = self._prepare_participating_query(user_id, statuses=statuses)
388 return q.count()
388 return q.count()
389
389
390 def get_im_participating_in(
390 def get_im_participating_in(
391 self, user_id=None, statuses=None, offset=0,
391 self, user_id=None, statuses=None, offset=0,
392 length=None, order_by=None, order_dir='desc'):
392 length=None, order_by=None, order_dir='desc'):
393 """
393 """
394 Get all Pull requests that i'm participating in, or i have opened
394 Get all Pull requests that i'm participating in, or i have opened
395 """
395 """
396
396
397 q = self._prepare_participating_query(
397 q = self._prepare_participating_query(
398 user_id, statuses=statuses, order_by=order_by,
398 user_id, statuses=statuses, order_by=order_by,
399 order_dir=order_dir)
399 order_dir=order_dir)
400
400
401 if length:
401 if length:
402 pull_requests = q.limit(length).offset(offset).all()
402 pull_requests = q.limit(length).offset(offset).all()
403 else:
403 else:
404 pull_requests = q.all()
404 pull_requests = q.all()
405
405
406 return pull_requests
406 return pull_requests
407
407
408 def get_versions(self, pull_request):
408 def get_versions(self, pull_request):
409 """
409 """
410 returns version of pull request sorted by ID descending
410 returns version of pull request sorted by ID descending
411 """
411 """
412 return PullRequestVersion.query()\
412 return PullRequestVersion.query()\
413 .filter(PullRequestVersion.pull_request == pull_request)\
413 .filter(PullRequestVersion.pull_request == pull_request)\
414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
415 .all()
415 .all()
416
416
417 def create(self, created_by, source_repo, source_ref, target_repo,
417 def create(self, created_by, source_repo, source_ref, target_repo,
418 target_ref, revisions, reviewers, title, description=None):
418 target_ref, revisions, reviewers, title, description=None):
419 created_by_user = self._get_user(created_by)
419 created_by_user = self._get_user(created_by)
420 source_repo = self._get_repo(source_repo)
420 source_repo = self._get_repo(source_repo)
421 target_repo = self._get_repo(target_repo)
421 target_repo = self._get_repo(target_repo)
422
422
423 pull_request = PullRequest()
423 pull_request = PullRequest()
424 pull_request.source_repo = source_repo
424 pull_request.source_repo = source_repo
425 pull_request.source_ref = source_ref
425 pull_request.source_ref = source_ref
426 pull_request.target_repo = target_repo
426 pull_request.target_repo = target_repo
427 pull_request.target_ref = target_ref
427 pull_request.target_ref = target_ref
428 pull_request.revisions = revisions
428 pull_request.revisions = revisions
429 pull_request.title = title
429 pull_request.title = title
430 pull_request.description = description
430 pull_request.description = description
431 pull_request.author = created_by_user
431 pull_request.author = created_by_user
432
432
433 Session().add(pull_request)
433 Session().add(pull_request)
434 Session().flush()
434 Session().flush()
435
435
436 reviewer_ids = set()
436 reviewer_ids = set()
437 # members / reviewers
437 # members / reviewers
438 for reviewer_object in reviewers:
438 for reviewer_object in reviewers:
439 if isinstance(reviewer_object, tuple):
439 if isinstance(reviewer_object, tuple):
440 user_id, reasons = reviewer_object
440 user_id, reasons = reviewer_object
441 else:
441 else:
442 user_id, reasons = reviewer_object, []
442 user_id, reasons = reviewer_object, []
443
443
444 user = self._get_user(user_id)
444 user = self._get_user(user_id)
445 reviewer_ids.add(user.user_id)
445 reviewer_ids.add(user.user_id)
446
446
447 reviewer = PullRequestReviewers(user, pull_request, reasons)
447 reviewer = PullRequestReviewers(user, pull_request, reasons)
448 Session().add(reviewer)
448 Session().add(reviewer)
449
449
450 # Set approval status to "Under Review" for all commits which are
450 # Set approval status to "Under Review" for all commits which are
451 # part of this pull request.
451 # part of this pull request.
452 ChangesetStatusModel().set_status(
452 ChangesetStatusModel().set_status(
453 repo=target_repo,
453 repo=target_repo,
454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
455 user=created_by_user,
455 user=created_by_user,
456 pull_request=pull_request
456 pull_request=pull_request
457 )
457 )
458
458
459 self.notify_reviewers(pull_request, reviewer_ids)
459 self.notify_reviewers(pull_request, reviewer_ids)
460 self._trigger_pull_request_hook(
460 self._trigger_pull_request_hook(
461 pull_request, created_by_user, 'create')
461 pull_request, created_by_user, 'create')
462
462
463 return pull_request
463 return pull_request
464
464
465 def _trigger_pull_request_hook(self, pull_request, user, action):
465 def _trigger_pull_request_hook(self, pull_request, user, action):
466 pull_request = self.__get_pull_request(pull_request)
466 pull_request = self.__get_pull_request(pull_request)
467 target_scm = pull_request.target_repo.scm_instance()
467 target_scm = pull_request.target_repo.scm_instance()
468 if action == 'create':
468 if action == 'create':
469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
470 elif action == 'merge':
470 elif action == 'merge':
471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
472 elif action == 'close':
472 elif action == 'close':
473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
474 elif action == 'review_status_change':
474 elif action == 'review_status_change':
475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
476 elif action == 'update':
476 elif action == 'update':
477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
478 else:
478 else:
479 return
479 return
480
480
481 trigger_hook(
481 trigger_hook(
482 username=user.username,
482 username=user.username,
483 repo_name=pull_request.target_repo.repo_name,
483 repo_name=pull_request.target_repo.repo_name,
484 repo_alias=target_scm.alias,
484 repo_alias=target_scm.alias,
485 pull_request=pull_request)
485 pull_request=pull_request)
486
486
487 def _get_commit_ids(self, pull_request):
487 def _get_commit_ids(self, pull_request):
488 """
488 """
489 Return the commit ids of the merged pull request.
489 Return the commit ids of the merged pull request.
490
490
491 This method is not dealing correctly yet with the lack of autoupdates
491 This method is not dealing correctly yet with the lack of autoupdates
492 nor with the implicit target updates.
492 nor with the implicit target updates.
493 For example: if a commit in the source repo is already in the target it
493 For example: if a commit in the source repo is already in the target it
494 will be reported anyways.
494 will be reported anyways.
495 """
495 """
496 merge_rev = pull_request.merge_rev
496 merge_rev = pull_request.merge_rev
497 if merge_rev is None:
497 if merge_rev is None:
498 raise ValueError('This pull request was not merged yet')
498 raise ValueError('This pull request was not merged yet')
499
499
500 commit_ids = list(pull_request.revisions)
500 commit_ids = list(pull_request.revisions)
501 if merge_rev not in commit_ids:
501 if merge_rev not in commit_ids:
502 commit_ids.append(merge_rev)
502 commit_ids.append(merge_rev)
503
503
504 return commit_ids
504 return commit_ids
505
505
506 def merge(self, pull_request, user, extras):
506 def merge(self, pull_request, user, extras):
507 log.debug("Merging pull request %s", pull_request.pull_request_id)
507 log.debug("Merging pull request %s", pull_request.pull_request_id)
508 merge_state = self._merge_pull_request(pull_request, user, extras)
508 merge_state = self._merge_pull_request(pull_request, user, extras)
509 if merge_state.executed:
509 if merge_state.executed:
510 log.debug(
510 log.debug(
511 "Merge was successful, updating the pull request comments.")
511 "Merge was successful, updating the pull request comments.")
512 self._comment_and_close_pr(pull_request, user, merge_state)
512 self._comment_and_close_pr(pull_request, user, merge_state)
513 self._log_action('user_merged_pull_request', user, pull_request)
513 self._log_action('user_merged_pull_request', user, pull_request)
514 else:
514 else:
515 log.warn("Merge failed, not updating the pull request.")
515 log.warn("Merge failed, not updating the pull request.")
516 return merge_state
516 return merge_state
517
517
518 def _merge_pull_request(self, pull_request, user, extras):
518 def _merge_pull_request(self, pull_request, user, extras):
519 target_vcs = pull_request.target_repo.scm_instance()
519 target_vcs = pull_request.target_repo.scm_instance()
520 source_vcs = pull_request.source_repo.scm_instance()
520 source_vcs = pull_request.source_repo.scm_instance()
521 target_ref = self._refresh_reference(
521 target_ref = self._refresh_reference(
522 pull_request.target_ref_parts, target_vcs)
522 pull_request.target_ref_parts, target_vcs)
523
523
524 message = _(
524 message = _(
525 'Merge pull request #%(pr_id)s from '
525 'Merge pull request #%(pr_id)s from '
526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
527 'pr_id': pull_request.pull_request_id,
527 'pr_id': pull_request.pull_request_id,
528 'source_repo': source_vcs.name,
528 'source_repo': source_vcs.name,
529 'source_ref_name': pull_request.source_ref_parts.name,
529 'source_ref_name': pull_request.source_ref_parts.name,
530 'pr_title': pull_request.title
530 'pr_title': pull_request.title
531 }
531 }
532
532
533 workspace_id = self._workspace_id(pull_request)
533 workspace_id = self._workspace_id(pull_request)
534 use_rebase = self._use_rebase_for_merging(pull_request)
534 use_rebase = self._use_rebase_for_merging(pull_request)
535
535
536 callback_daemon, extras = prepare_callback_daemon(
536 callback_daemon, extras = prepare_callback_daemon(
537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
539
539
540 with callback_daemon:
540 with callback_daemon:
541 # TODO: johbo: Implement a clean way to run a config_override
541 # TODO: johbo: Implement a clean way to run a config_override
542 # for a single call.
542 # for a single call.
543 target_vcs.config.set(
543 target_vcs.config.set(
544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
545 merge_state = target_vcs.merge(
545 merge_state = target_vcs.merge(
546 target_ref, source_vcs, pull_request.source_ref_parts,
546 target_ref, source_vcs, pull_request.source_ref_parts,
547 workspace_id, user_name=user.username,
547 workspace_id, user_name=user.username,
548 user_email=user.email, message=message, use_rebase=use_rebase)
548 user_email=user.email, message=message, use_rebase=use_rebase)
549 return merge_state
549 return merge_state
550
550
551 def _comment_and_close_pr(self, pull_request, user, merge_state):
551 def _comment_and_close_pr(self, pull_request, user, merge_state):
552 pull_request.merge_rev = merge_state.merge_ref.commit_id
552 pull_request.merge_rev = merge_state.merge_ref.commit_id
553 pull_request.updated_on = datetime.datetime.now()
553 pull_request.updated_on = datetime.datetime.now()
554
554
555 CommentsModel().create(
555 CommentsModel().create(
556 text=unicode(_('Pull request merged and closed')),
556 text=unicode(_('Pull request merged and closed')),
557 repo=pull_request.target_repo.repo_id,
557 repo=pull_request.target_repo.repo_id,
558 user=user.user_id,
558 user=user.user_id,
559 pull_request=pull_request.pull_request_id,
559 pull_request=pull_request.pull_request_id,
560 f_path=None,
560 f_path=None,
561 line_no=None,
561 line_no=None,
562 closing_pr=True
562 closing_pr=True
563 )
563 )
564
564
565 Session().add(pull_request)
565 Session().add(pull_request)
566 Session().flush()
566 Session().flush()
567 # TODO: paris: replace invalidation with less radical solution
567 # TODO: paris: replace invalidation with less radical solution
568 ScmModel().mark_for_invalidation(
568 ScmModel().mark_for_invalidation(
569 pull_request.target_repo.repo_name)
569 pull_request.target_repo.repo_name)
570 self._trigger_pull_request_hook(pull_request, user, 'merge')
570 self._trigger_pull_request_hook(pull_request, user, 'merge')
571
571
572 def has_valid_update_type(self, pull_request):
572 def has_valid_update_type(self, pull_request):
573 source_ref_type = pull_request.source_ref_parts.type
573 source_ref_type = pull_request.source_ref_parts.type
574 return source_ref_type in ['book', 'branch', 'tag']
574 return source_ref_type in ['book', 'branch', 'tag']
575
575
576 def update_commits(self, pull_request):
576 def update_commits(self, pull_request):
577 """
577 """
578 Get the updated list of commits for the pull request
578 Get the updated list of commits for the pull request
579 and return the new pull request version and the list
579 and return the new pull request version and the list
580 of commits processed by this update action
580 of commits processed by this update action
581 """
581 """
582 pull_request = self.__get_pull_request(pull_request)
582 pull_request = self.__get_pull_request(pull_request)
583 source_ref_type = pull_request.source_ref_parts.type
583 source_ref_type = pull_request.source_ref_parts.type
584 source_ref_name = pull_request.source_ref_parts.name
584 source_ref_name = pull_request.source_ref_parts.name
585 source_ref_id = pull_request.source_ref_parts.commit_id
585 source_ref_id = pull_request.source_ref_parts.commit_id
586
586
587 if not self.has_valid_update_type(pull_request):
587 if not self.has_valid_update_type(pull_request):
588 log.debug(
588 log.debug(
589 "Skipping update of pull request %s due to ref type: %s",
589 "Skipping update of pull request %s due to ref type: %s",
590 pull_request, source_ref_type)
590 pull_request, source_ref_type)
591 return UpdateResponse(
591 return UpdateResponse(
592 executed=False,
592 executed=False,
593 reason=UpdateFailureReason.WRONG_REF_TPYE,
593 reason=UpdateFailureReason.WRONG_REF_TPYE,
594 old=pull_request, new=None, changes=None)
594 old=pull_request, new=None, changes=None)
595
595
596 source_repo = pull_request.source_repo.scm_instance()
596 source_repo = pull_request.source_repo.scm_instance()
597 try:
597 try:
598 source_commit = source_repo.get_commit(commit_id=source_ref_name)
598 source_commit = source_repo.get_commit(commit_id=source_ref_name)
599 except CommitDoesNotExistError:
599 except CommitDoesNotExistError:
600 return UpdateResponse(
600 return UpdateResponse(
601 executed=False,
601 executed=False,
602 reason=UpdateFailureReason.MISSING_SOURCE_REF,
602 reason=UpdateFailureReason.MISSING_SOURCE_REF,
603 old=pull_request, new=None, changes=None)
603 old=pull_request, new=None, changes=None)
604
604
605 if source_ref_id == source_commit.raw_id:
605 if source_ref_id == source_commit.raw_id:
606 log.debug("Nothing changed in pull request %s", pull_request)
606 log.debug("Nothing changed in pull request %s", pull_request)
607 return UpdateResponse(
607 return UpdateResponse(
608 executed=False,
608 executed=False,
609 reason=UpdateFailureReason.NO_CHANGE,
609 reason=UpdateFailureReason.NO_CHANGE,
610 old=pull_request, new=None, changes=None)
610 old=pull_request, new=None, changes=None)
611
611
612 # Finally there is a need for an update
612 # Finally there is a need for an update
613 pull_request_version = self._create_version_from_snapshot(pull_request)
613 pull_request_version = self._create_version_from_snapshot(pull_request)
614 self._link_comments_to_version(pull_request_version)
614 self._link_comments_to_version(pull_request_version)
615
615
616 target_ref_type = pull_request.target_ref_parts.type
616 target_ref_type = pull_request.target_ref_parts.type
617 target_ref_name = pull_request.target_ref_parts.name
617 target_ref_name = pull_request.target_ref_parts.name
618 target_ref_id = pull_request.target_ref_parts.commit_id
618 target_ref_id = pull_request.target_ref_parts.commit_id
619 target_repo = pull_request.target_repo.scm_instance()
619 target_repo = pull_request.target_repo.scm_instance()
620
620
621 try:
621 try:
622 if target_ref_type in ('tag', 'branch', 'book'):
622 if target_ref_type in ('tag', 'branch', 'book'):
623 target_commit = target_repo.get_commit(target_ref_name)
623 target_commit = target_repo.get_commit(target_ref_name)
624 else:
624 else:
625 target_commit = target_repo.get_commit(target_ref_id)
625 target_commit = target_repo.get_commit(target_ref_id)
626 except CommitDoesNotExistError:
626 except CommitDoesNotExistError:
627 return UpdateResponse(
627 return UpdateResponse(
628 executed=False,
628 executed=False,
629 reason=UpdateFailureReason.MISSING_TARGET_REF,
629 reason=UpdateFailureReason.MISSING_TARGET_REF,
630 old=pull_request, new=None, changes=None)
630 old=pull_request, new=None, changes=None)
631
631
632 # re-compute commit ids
632 # re-compute commit ids
633 old_commit_ids = pull_request.revisions
633 old_commit_ids = pull_request.revisions
634 pre_load = ["author", "branch", "date", "message"]
634 pre_load = ["author", "branch", "date", "message"]
635 commit_ranges = target_repo.compare(
635 commit_ranges = target_repo.compare(
636 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
636 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
637 pre_load=pre_load)
637 pre_load=pre_load)
638
638
639 ancestor = target_repo.get_common_ancestor(
639 ancestor = target_repo.get_common_ancestor(
640 target_commit.raw_id, source_commit.raw_id, source_repo)
640 target_commit.raw_id, source_commit.raw_id, source_repo)
641
641
642 pull_request.source_ref = '%s:%s:%s' % (
642 pull_request.source_ref = '%s:%s:%s' % (
643 source_ref_type, source_ref_name, source_commit.raw_id)
643 source_ref_type, source_ref_name, source_commit.raw_id)
644 pull_request.target_ref = '%s:%s:%s' % (
644 pull_request.target_ref = '%s:%s:%s' % (
645 target_ref_type, target_ref_name, ancestor)
645 target_ref_type, target_ref_name, ancestor)
646 pull_request.revisions = [
646 pull_request.revisions = [
647 commit.raw_id for commit in reversed(commit_ranges)]
647 commit.raw_id for commit in reversed(commit_ranges)]
648 pull_request.updated_on = datetime.datetime.now()
648 pull_request.updated_on = datetime.datetime.now()
649 Session().add(pull_request)
649 Session().add(pull_request)
650 new_commit_ids = pull_request.revisions
650 new_commit_ids = pull_request.revisions
651
651
652 changes = self._calculate_commit_id_changes(
652 changes = self._calculate_commit_id_changes(
653 old_commit_ids, new_commit_ids)
653 old_commit_ids, new_commit_ids)
654
654
655 old_diff_data, new_diff_data = self._generate_update_diffs(
655 old_diff_data, new_diff_data = self._generate_update_diffs(
656 pull_request, pull_request_version)
656 pull_request, pull_request_version)
657
657
658 CommentsModel().outdate_comments(
658 CommentsModel().outdate_comments(
659 pull_request, old_diff_data=old_diff_data,
659 pull_request, old_diff_data=old_diff_data,
660 new_diff_data=new_diff_data)
660 new_diff_data=new_diff_data)
661
661
662 file_changes = self._calculate_file_changes(
662 file_changes = self._calculate_file_changes(
663 old_diff_data, new_diff_data)
663 old_diff_data, new_diff_data)
664
664
665 # Add an automatic comment to the pull request
665 # Add an automatic comment to the pull request
666 update_comment = CommentsModel().create(
666 update_comment = CommentsModel().create(
667 text=self._render_update_message(changes, file_changes),
667 text=self._render_update_message(changes, file_changes),
668 repo=pull_request.target_repo,
668 repo=pull_request.target_repo,
669 user=pull_request.author,
669 user=pull_request.author,
670 pull_request=pull_request,
670 pull_request=pull_request,
671 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
671 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
672
672
673 # Update status to "Under Review" for added commits
673 # Update status to "Under Review" for added commits
674 for commit_id in changes.added:
674 for commit_id in changes.added:
675 ChangesetStatusModel().set_status(
675 ChangesetStatusModel().set_status(
676 repo=pull_request.source_repo,
676 repo=pull_request.source_repo,
677 status=ChangesetStatus.STATUS_UNDER_REVIEW,
677 status=ChangesetStatus.STATUS_UNDER_REVIEW,
678 comment=update_comment,
678 comment=update_comment,
679 user=pull_request.author,
679 user=pull_request.author,
680 pull_request=pull_request,
680 pull_request=pull_request,
681 revision=commit_id)
681 revision=commit_id)
682
682
683 log.debug(
683 log.debug(
684 'Updated pull request %s, added_ids: %s, common_ids: %s, '
684 'Updated pull request %s, added_ids: %s, common_ids: %s, '
685 'removed_ids: %s', pull_request.pull_request_id,
685 'removed_ids: %s', pull_request.pull_request_id,
686 changes.added, changes.common, changes.removed)
686 changes.added, changes.common, changes.removed)
687 log.debug('Updated pull request with the following file changes: %s',
687 log.debug('Updated pull request with the following file changes: %s',
688 file_changes)
688 file_changes)
689
689
690 log.info(
690 log.info(
691 "Updated pull request %s from commit %s to commit %s, "
691 "Updated pull request %s from commit %s to commit %s, "
692 "stored new version %s of this pull request.",
692 "stored new version %s of this pull request.",
693 pull_request.pull_request_id, source_ref_id,
693 pull_request.pull_request_id, source_ref_id,
694 pull_request.source_ref_parts.commit_id,
694 pull_request.source_ref_parts.commit_id,
695 pull_request_version.pull_request_version_id)
695 pull_request_version.pull_request_version_id)
696 Session().commit()
696 Session().commit()
697 self._trigger_pull_request_hook(pull_request, pull_request.author,
697 self._trigger_pull_request_hook(pull_request, pull_request.author,
698 'update')
698 'update')
699
699
700 return UpdateResponse(
700 return UpdateResponse(
701 executed=True, reason=UpdateFailureReason.NONE,
701 executed=True, reason=UpdateFailureReason.NONE,
702 old=pull_request, new=pull_request_version, changes=changes)
702 old=pull_request, new=pull_request_version, changes=changes)
703
703
704 def _create_version_from_snapshot(self, pull_request):
704 def _create_version_from_snapshot(self, pull_request):
705 version = PullRequestVersion()
705 version = PullRequestVersion()
706 version.title = pull_request.title
706 version.title = pull_request.title
707 version.description = pull_request.description
707 version.description = pull_request.description
708 version.status = pull_request.status
708 version.status = pull_request.status
709 version.created_on = datetime.datetime.now()
709 version.created_on = datetime.datetime.now()
710 version.updated_on = pull_request.updated_on
710 version.updated_on = pull_request.updated_on
711 version.user_id = pull_request.user_id
711 version.user_id = pull_request.user_id
712 version.source_repo = pull_request.source_repo
712 version.source_repo = pull_request.source_repo
713 version.source_ref = pull_request.source_ref
713 version.source_ref = pull_request.source_ref
714 version.target_repo = pull_request.target_repo
714 version.target_repo = pull_request.target_repo
715 version.target_ref = pull_request.target_ref
715 version.target_ref = pull_request.target_ref
716
716
717 version._last_merge_source_rev = pull_request._last_merge_source_rev
717 version._last_merge_source_rev = pull_request._last_merge_source_rev
718 version._last_merge_target_rev = pull_request._last_merge_target_rev
718 version._last_merge_target_rev = pull_request._last_merge_target_rev
719 version._last_merge_status = pull_request._last_merge_status
719 version._last_merge_status = pull_request._last_merge_status
720 version.shadow_merge_ref = pull_request.shadow_merge_ref
720 version.shadow_merge_ref = pull_request.shadow_merge_ref
721 version.merge_rev = pull_request.merge_rev
721 version.merge_rev = pull_request.merge_rev
722
722
723 version.revisions = pull_request.revisions
723 version.revisions = pull_request.revisions
724 version.pull_request = pull_request
724 version.pull_request = pull_request
725 Session().add(version)
725 Session().add(version)
726 Session().flush()
726 Session().flush()
727
727
728 return version
728 return version
729
729
730 def _generate_update_diffs(self, pull_request, pull_request_version):
730 def _generate_update_diffs(self, pull_request, pull_request_version):
731
731
732 diff_context = (
732 diff_context = (
733 self.DIFF_CONTEXT +
733 self.DIFF_CONTEXT +
734 CommentsModel.needed_extra_diff_context())
734 CommentsModel.needed_extra_diff_context())
735
735
736 source_repo = pull_request_version.source_repo
736 source_repo = pull_request_version.source_repo
737 source_ref_id = pull_request_version.source_ref_parts.commit_id
737 source_ref_id = pull_request_version.source_ref_parts.commit_id
738 target_ref_id = pull_request_version.target_ref_parts.commit_id
738 target_ref_id = pull_request_version.target_ref_parts.commit_id
739 old_diff = self._get_diff_from_pr_or_version(
739 old_diff = self._get_diff_from_pr_or_version(
740 source_repo, source_ref_id, target_ref_id, context=diff_context)
740 source_repo, source_ref_id, target_ref_id, context=diff_context)
741
741
742 source_repo = pull_request.source_repo
742 source_repo = pull_request.source_repo
743 source_ref_id = pull_request.source_ref_parts.commit_id
743 source_ref_id = pull_request.source_ref_parts.commit_id
744 target_ref_id = pull_request.target_ref_parts.commit_id
744 target_ref_id = pull_request.target_ref_parts.commit_id
745
745
746 new_diff = self._get_diff_from_pr_or_version(
746 new_diff = self._get_diff_from_pr_or_version(
747 source_repo, source_ref_id, target_ref_id, context=diff_context)
747 source_repo, source_ref_id, target_ref_id, context=diff_context)
748
748
749 old_diff_data = diffs.DiffProcessor(old_diff)
749 old_diff_data = diffs.DiffProcessor(old_diff)
750 old_diff_data.prepare()
750 old_diff_data.prepare()
751 new_diff_data = diffs.DiffProcessor(new_diff)
751 new_diff_data = diffs.DiffProcessor(new_diff)
752 new_diff_data.prepare()
752 new_diff_data.prepare()
753
753
754 return old_diff_data, new_diff_data
754 return old_diff_data, new_diff_data
755
755
756 def _link_comments_to_version(self, pull_request_version):
756 def _link_comments_to_version(self, pull_request_version):
757 """
757 """
758 Link all unlinked comments of this pull request to the given version.
758 Link all unlinked comments of this pull request to the given version.
759
759
760 :param pull_request_version: The `PullRequestVersion` to which
760 :param pull_request_version: The `PullRequestVersion` to which
761 the comments shall be linked.
761 the comments shall be linked.
762
762
763 """
763 """
764 pull_request = pull_request_version.pull_request
764 pull_request = pull_request_version.pull_request
765 comments = ChangesetComment.query().filter(
765 comments = ChangesetComment.query().filter(
766 # TODO: johbo: Should we query for the repo at all here?
766 # TODO: johbo: Should we query for the repo at all here?
767 # Pending decision on how comments of PRs are to be related
767 # Pending decision on how comments of PRs are to be related
768 # to either the source repo, the target repo or no repo at all.
768 # to either the source repo, the target repo or no repo at all.
769 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
769 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
770 ChangesetComment.pull_request == pull_request,
770 ChangesetComment.pull_request == pull_request,
771 ChangesetComment.pull_request_version == None)
771 ChangesetComment.pull_request_version == None)
772
772
773 # TODO: johbo: Find out why this breaks if it is done in a bulk
773 # TODO: johbo: Find out why this breaks if it is done in a bulk
774 # operation.
774 # operation.
775 for comment in comments:
775 for comment in comments:
776 comment.pull_request_version_id = (
776 comment.pull_request_version_id = (
777 pull_request_version.pull_request_version_id)
777 pull_request_version.pull_request_version_id)
778 Session().add(comment)
778 Session().add(comment)
779
779
780 def _calculate_commit_id_changes(self, old_ids, new_ids):
780 def _calculate_commit_id_changes(self, old_ids, new_ids):
781 added = [x for x in new_ids if x not in old_ids]
781 added = [x for x in new_ids if x not in old_ids]
782 common = [x for x in new_ids if x in old_ids]
782 common = [x for x in new_ids if x in old_ids]
783 removed = [x for x in old_ids if x not in new_ids]
783 removed = [x for x in old_ids if x not in new_ids]
784 total = new_ids
784 total = new_ids
785 return ChangeTuple(added, common, removed, total)
785 return ChangeTuple(added, common, removed, total)
786
786
787 def _calculate_file_changes(self, old_diff_data, new_diff_data):
787 def _calculate_file_changes(self, old_diff_data, new_diff_data):
788
788
789 old_files = OrderedDict()
789 old_files = OrderedDict()
790 for diff_data in old_diff_data.parsed_diff:
790 for diff_data in old_diff_data.parsed_diff:
791 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
791 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
792
792
793 added_files = []
793 added_files = []
794 modified_files = []
794 modified_files = []
795 removed_files = []
795 removed_files = []
796 for diff_data in new_diff_data.parsed_diff:
796 for diff_data in new_diff_data.parsed_diff:
797 new_filename = diff_data['filename']
797 new_filename = diff_data['filename']
798 new_hash = md5_safe(diff_data['raw_diff'])
798 new_hash = md5_safe(diff_data['raw_diff'])
799
799
800 old_hash = old_files.get(new_filename)
800 old_hash = old_files.get(new_filename)
801 if not old_hash:
801 if not old_hash:
802 # file is not present in old diff, means it's added
802 # file is not present in old diff, means it's added
803 added_files.append(new_filename)
803 added_files.append(new_filename)
804 else:
804 else:
805 if new_hash != old_hash:
805 if new_hash != old_hash:
806 modified_files.append(new_filename)
806 modified_files.append(new_filename)
807 # now remove a file from old, since we have seen it already
807 # now remove a file from old, since we have seen it already
808 del old_files[new_filename]
808 del old_files[new_filename]
809
809
810 # removed files is when there are present in old, but not in NEW,
810 # removed files is when there are present in old, but not in NEW,
811 # since we remove old files that are present in new diff, left-overs
811 # since we remove old files that are present in new diff, left-overs
812 # if any should be the removed files
812 # if any should be the removed files
813 removed_files.extend(old_files.keys())
813 removed_files.extend(old_files.keys())
814
814
815 return FileChangeTuple(added_files, modified_files, removed_files)
815 return FileChangeTuple(added_files, modified_files, removed_files)
816
816
817 def _render_update_message(self, changes, file_changes):
817 def _render_update_message(self, changes, file_changes):
818 """
818 """
819 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
819 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
820 so it's always looking the same disregarding on which default
820 so it's always looking the same disregarding on which default
821 renderer system is using.
821 renderer system is using.
822
822
823 :param changes: changes named tuple
823 :param changes: changes named tuple
824 :param file_changes: file changes named tuple
824 :param file_changes: file changes named tuple
825
825
826 """
826 """
827 new_status = ChangesetStatus.get_status_lbl(
827 new_status = ChangesetStatus.get_status_lbl(
828 ChangesetStatus.STATUS_UNDER_REVIEW)
828 ChangesetStatus.STATUS_UNDER_REVIEW)
829
829
830 changed_files = (
830 changed_files = (
831 file_changes.added + file_changes.modified + file_changes.removed)
831 file_changes.added + file_changes.modified + file_changes.removed)
832
832
833 params = {
833 params = {
834 'under_review_label': new_status,
834 'under_review_label': new_status,
835 'added_commits': changes.added,
835 'added_commits': changes.added,
836 'removed_commits': changes.removed,
836 'removed_commits': changes.removed,
837 'changed_files': changed_files,
837 'changed_files': changed_files,
838 'added_files': file_changes.added,
838 'added_files': file_changes.added,
839 'modified_files': file_changes.modified,
839 'modified_files': file_changes.modified,
840 'removed_files': file_changes.removed,
840 'removed_files': file_changes.removed,
841 }
841 }
842 renderer = RstTemplateRenderer()
842 renderer = RstTemplateRenderer()
843 return renderer.render('pull_request_update.mako', **params)
843 return renderer.render('pull_request_update.mako', **params)
844
844
845 def edit(self, pull_request, title, description):
845 def edit(self, pull_request, title, description):
846 pull_request = self.__get_pull_request(pull_request)
846 pull_request = self.__get_pull_request(pull_request)
847 if pull_request.is_closed():
847 if pull_request.is_closed():
848 raise ValueError('This pull request is closed')
848 raise ValueError('This pull request is closed')
849 if title:
849 if title:
850 pull_request.title = title
850 pull_request.title = title
851 pull_request.description = description
851 pull_request.description = description
852 pull_request.updated_on = datetime.datetime.now()
852 pull_request.updated_on = datetime.datetime.now()
853 Session().add(pull_request)
853 Session().add(pull_request)
854
854
855 def update_reviewers(self, pull_request, reviewer_data):
855 def update_reviewers(self, pull_request, reviewer_data):
856 """
856 """
857 Update the reviewers in the pull request
857 Update the reviewers in the pull request
858
858
859 :param pull_request: the pr to update
859 :param pull_request: the pr to update
860 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
860 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
861 """
861 """
862
862
863 reviewers_reasons = {}
863 reviewers_reasons = {}
864 for user_id, reasons in reviewer_data:
864 for user_id, reasons in reviewer_data:
865 if isinstance(user_id, (int, basestring)):
865 if isinstance(user_id, (int, basestring)):
866 user_id = self._get_user(user_id).user_id
866 user_id = self._get_user(user_id).user_id
867 reviewers_reasons[user_id] = reasons
867 reviewers_reasons[user_id] = reasons
868
868
869 reviewers_ids = set(reviewers_reasons.keys())
869 reviewers_ids = set(reviewers_reasons.keys())
870 pull_request = self.__get_pull_request(pull_request)
870 pull_request = self.__get_pull_request(pull_request)
871 current_reviewers = PullRequestReviewers.query()\
871 current_reviewers = PullRequestReviewers.query()\
872 .filter(PullRequestReviewers.pull_request ==
872 .filter(PullRequestReviewers.pull_request ==
873 pull_request).all()
873 pull_request).all()
874 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
874 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
875
875
876 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
876 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
877 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
877 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
878
878
879 log.debug("Adding %s reviewers", ids_to_add)
879 log.debug("Adding %s reviewers", ids_to_add)
880 log.debug("Removing %s reviewers", ids_to_remove)
880 log.debug("Removing %s reviewers", ids_to_remove)
881 changed = False
881 changed = False
882 for uid in ids_to_add:
882 for uid in ids_to_add:
883 changed = True
883 changed = True
884 _usr = self._get_user(uid)
884 _usr = self._get_user(uid)
885 reasons = reviewers_reasons[uid]
885 reasons = reviewers_reasons[uid]
886 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
886 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
887 Session().add(reviewer)
887 Session().add(reviewer)
888
888
889 self.notify_reviewers(pull_request, ids_to_add)
889 self.notify_reviewers(pull_request, ids_to_add)
890
890
891 for uid in ids_to_remove:
891 for uid in ids_to_remove:
892 changed = True
892 changed = True
893 reviewer = PullRequestReviewers.query()\
893 reviewer = PullRequestReviewers.query()\
894 .filter(PullRequestReviewers.user_id == uid,
894 .filter(PullRequestReviewers.user_id == uid,
895 PullRequestReviewers.pull_request == pull_request)\
895 PullRequestReviewers.pull_request == pull_request)\
896 .scalar()
896 .scalar()
897 if reviewer:
897 if reviewer:
898 Session().delete(reviewer)
898 Session().delete(reviewer)
899 if changed:
899 if changed:
900 pull_request.updated_on = datetime.datetime.now()
900 pull_request.updated_on = datetime.datetime.now()
901 Session().add(pull_request)
901 Session().add(pull_request)
902
902
903 return ids_to_add, ids_to_remove
903 return ids_to_add, ids_to_remove
904
904
905 def get_url(self, pull_request):
905 def get_url(self, pull_request):
906 return h.url('pullrequest_show',
906 return h.url('pullrequest_show',
907 repo_name=safe_str(pull_request.target_repo.repo_name),
907 repo_name=safe_str(pull_request.target_repo.repo_name),
908 pull_request_id=pull_request.pull_request_id,
908 pull_request_id=pull_request.pull_request_id,
909 qualified=True)
909 qualified=True)
910
910
911 def get_shadow_clone_url(self, pull_request):
911 def get_shadow_clone_url(self, pull_request):
912 """
912 """
913 Returns qualified url pointing to the shadow repository. If this pull
913 Returns qualified url pointing to the shadow repository. If this pull
914 request is closed there is no shadow repository and ``None`` will be
914 request is closed there is no shadow repository and ``None`` will be
915 returned.
915 returned.
916 """
916 """
917 if pull_request.is_closed():
917 if pull_request.is_closed():
918 return None
918 return None
919 else:
919 else:
920 pr_url = urllib.unquote(self.get_url(pull_request))
920 pr_url = urllib.unquote(self.get_url(pull_request))
921 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
921 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
922
922
923 def notify_reviewers(self, pull_request, reviewers_ids):
923 def notify_reviewers(self, pull_request, reviewers_ids):
924 # notification to reviewers
924 # notification to reviewers
925 if not reviewers_ids:
925 if not reviewers_ids:
926 return
926 return
927
927
928 pull_request_obj = pull_request
928 pull_request_obj = pull_request
929 # get the current participants of this pull request
929 # get the current participants of this pull request
930 recipients = reviewers_ids
930 recipients = reviewers_ids
931 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
931 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
932
932
933 pr_source_repo = pull_request_obj.source_repo
933 pr_source_repo = pull_request_obj.source_repo
934 pr_target_repo = pull_request_obj.target_repo
934 pr_target_repo = pull_request_obj.target_repo
935
935
936 pr_url = h.url(
936 pr_url = h.url(
937 'pullrequest_show',
937 'pullrequest_show',
938 repo_name=pr_target_repo.repo_name,
938 repo_name=pr_target_repo.repo_name,
939 pull_request_id=pull_request_obj.pull_request_id,
939 pull_request_id=pull_request_obj.pull_request_id,
940 qualified=True,)
940 qualified=True,)
941
941
942 # set some variables for email notification
942 # set some variables for email notification
943 pr_target_repo_url = h.url(
943 pr_target_repo_url = h.url(
944 'summary_home',
944 'summary_home',
945 repo_name=pr_target_repo.repo_name,
945 repo_name=pr_target_repo.repo_name,
946 qualified=True)
946 qualified=True)
947
947
948 pr_source_repo_url = h.url(
948 pr_source_repo_url = h.url(
949 'summary_home',
949 'summary_home',
950 repo_name=pr_source_repo.repo_name,
950 repo_name=pr_source_repo.repo_name,
951 qualified=True)
951 qualified=True)
952
952
953 # pull request specifics
953 # pull request specifics
954 pull_request_commits = [
954 pull_request_commits = [
955 (x.raw_id, x.message)
955 (x.raw_id, x.message)
956 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
956 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
957
957
958 kwargs = {
958 kwargs = {
959 'user': pull_request.author,
959 'user': pull_request.author,
960 'pull_request': pull_request_obj,
960 'pull_request': pull_request_obj,
961 'pull_request_commits': pull_request_commits,
961 'pull_request_commits': pull_request_commits,
962
962
963 'pull_request_target_repo': pr_target_repo,
963 'pull_request_target_repo': pr_target_repo,
964 'pull_request_target_repo_url': pr_target_repo_url,
964 'pull_request_target_repo_url': pr_target_repo_url,
965
965
966 'pull_request_source_repo': pr_source_repo,
966 'pull_request_source_repo': pr_source_repo,
967 'pull_request_source_repo_url': pr_source_repo_url,
967 'pull_request_source_repo_url': pr_source_repo_url,
968
968
969 'pull_request_url': pr_url,
969 'pull_request_url': pr_url,
970 }
970 }
971
971
972 # pre-generate the subject for notification itself
972 # pre-generate the subject for notification itself
973 (subject,
973 (subject,
974 _h, _e, # we don't care about those
974 _h, _e, # we don't care about those
975 body_plaintext) = EmailNotificationModel().render_email(
975 body_plaintext) = EmailNotificationModel().render_email(
976 notification_type, **kwargs)
976 notification_type, **kwargs)
977
977
978 # create notification objects, and emails
978 # create notification objects, and emails
979 NotificationModel().create(
979 NotificationModel().create(
980 created_by=pull_request.author,
980 created_by=pull_request.author,
981 notification_subject=subject,
981 notification_subject=subject,
982 notification_body=body_plaintext,
982 notification_body=body_plaintext,
983 notification_type=notification_type,
983 notification_type=notification_type,
984 recipients=recipients,
984 recipients=recipients,
985 email_kwargs=kwargs,
985 email_kwargs=kwargs,
986 )
986 )
987
987
988 def delete(self, pull_request):
988 def delete(self, pull_request):
989 pull_request = self.__get_pull_request(pull_request)
989 pull_request = self.__get_pull_request(pull_request)
990 self._cleanup_merge_workspace(pull_request)
990 self._cleanup_merge_workspace(pull_request)
991 Session().delete(pull_request)
991 Session().delete(pull_request)
992
992
993 def close_pull_request(self, pull_request, user):
993 def close_pull_request(self, pull_request, user):
994 pull_request = self.__get_pull_request(pull_request)
994 pull_request = self.__get_pull_request(pull_request)
995 self._cleanup_merge_workspace(pull_request)
995 self._cleanup_merge_workspace(pull_request)
996 pull_request.status = PullRequest.STATUS_CLOSED
996 pull_request.status = PullRequest.STATUS_CLOSED
997 pull_request.updated_on = datetime.datetime.now()
997 pull_request.updated_on = datetime.datetime.now()
998 Session().add(pull_request)
998 Session().add(pull_request)
999 self._trigger_pull_request_hook(
999 self._trigger_pull_request_hook(
1000 pull_request, pull_request.author, 'close')
1000 pull_request, pull_request.author, 'close')
1001 self._log_action('user_closed_pull_request', user, pull_request)
1001 self._log_action('user_closed_pull_request', user, pull_request)
1002
1002
1003 def close_pull_request_with_comment(self, pull_request, user, repo,
1003 def close_pull_request_with_comment(self, pull_request, user, repo,
1004 message=None):
1004 message=None):
1005 status = ChangesetStatus.STATUS_REJECTED
1005 status = ChangesetStatus.STATUS_REJECTED
1006
1006
1007 if not message:
1007 if not message:
1008 message = (
1008 message = (
1009 _('Status change %(transition_icon)s %(status)s') % {
1009 _('Status change %(transition_icon)s %(status)s') % {
1010 'transition_icon': '>',
1010 'transition_icon': '>',
1011 'status': ChangesetStatus.get_status_lbl(status)})
1011 'status': ChangesetStatus.get_status_lbl(status)})
1012
1012
1013 internal_message = _('Closing with') + ' ' + message
1013 internal_message = _('Closing with') + ' ' + message
1014
1014
1015 comm = CommentsModel().create(
1015 comm = CommentsModel().create(
1016 text=internal_message,
1016 text=internal_message,
1017 repo=repo.repo_id,
1017 repo=repo.repo_id,
1018 user=user.user_id,
1018 user=user.user_id,
1019 pull_request=pull_request.pull_request_id,
1019 pull_request=pull_request.pull_request_id,
1020 f_path=None,
1020 f_path=None,
1021 line_no=None,
1021 line_no=None,
1022 status_change=ChangesetStatus.get_status_lbl(status),
1022 status_change=ChangesetStatus.get_status_lbl(status),
1023 status_change_type=status,
1023 status_change_type=status,
1024 closing_pr=True
1024 closing_pr=True
1025 )
1025 )
1026
1026
1027 ChangesetStatusModel().set_status(
1027 ChangesetStatusModel().set_status(
1028 repo.repo_id,
1028 repo.repo_id,
1029 status,
1029 status,
1030 user.user_id,
1030 user.user_id,
1031 comm,
1031 comm,
1032 pull_request=pull_request.pull_request_id
1032 pull_request=pull_request.pull_request_id
1033 )
1033 )
1034 Session().flush()
1034 Session().flush()
1035
1035
1036 PullRequestModel().close_pull_request(
1036 PullRequestModel().close_pull_request(
1037 pull_request.pull_request_id, user)
1037 pull_request.pull_request_id, user)
1038
1038
1039 def merge_status(self, pull_request):
1039 def merge_status(self, pull_request):
1040 if not self._is_merge_enabled(pull_request):
1040 if not self._is_merge_enabled(pull_request):
1041 return False, _('Server-side pull request merging is disabled.')
1041 return False, _('Server-side pull request merging is disabled.')
1042 if pull_request.is_closed():
1042 if pull_request.is_closed():
1043 return False, _('This pull request is closed.')
1043 return False, _('This pull request is closed.')
1044 merge_possible, msg = self._check_repo_requirements(
1044 merge_possible, msg = self._check_repo_requirements(
1045 target=pull_request.target_repo, source=pull_request.source_repo)
1045 target=pull_request.target_repo, source=pull_request.source_repo)
1046 if not merge_possible:
1046 if not merge_possible:
1047 return merge_possible, msg
1047 return merge_possible, msg
1048
1048
1049 try:
1049 try:
1050 resp = self._try_merge(pull_request)
1050 resp = self._try_merge(pull_request)
1051 log.debug("Merge response: %s", resp)
1051 log.debug("Merge response: %s", resp)
1052 status = resp.possible, self.merge_status_message(
1052 status = resp.possible, self.merge_status_message(
1053 resp.failure_reason)
1053 resp.failure_reason)
1054 except NotImplementedError:
1054 except NotImplementedError:
1055 status = False, _('Pull request merging is not supported.')
1055 status = False, _('Pull request merging is not supported.')
1056
1056
1057 return status
1057 return status
1058
1058
1059 def _check_repo_requirements(self, target, source):
1059 def _check_repo_requirements(self, target, source):
1060 """
1060 """
1061 Check if `target` and `source` have compatible requirements.
1061 Check if `target` and `source` have compatible requirements.
1062
1062
1063 Currently this is just checking for largefiles.
1063 Currently this is just checking for largefiles.
1064 """
1064 """
1065 target_has_largefiles = self._has_largefiles(target)
1065 target_has_largefiles = self._has_largefiles(target)
1066 source_has_largefiles = self._has_largefiles(source)
1066 source_has_largefiles = self._has_largefiles(source)
1067 merge_possible = True
1067 merge_possible = True
1068 message = u''
1068 message = u''
1069
1069
1070 if target_has_largefiles != source_has_largefiles:
1070 if target_has_largefiles != source_has_largefiles:
1071 merge_possible = False
1071 merge_possible = False
1072 if source_has_largefiles:
1072 if source_has_largefiles:
1073 message = _(
1073 message = _(
1074 'Target repository large files support is disabled.')
1074 'Target repository large files support is disabled.')
1075 else:
1075 else:
1076 message = _(
1076 message = _(
1077 'Source repository large files support is disabled.')
1077 'Source repository large files support is disabled.')
1078
1078
1079 return merge_possible, message
1079 return merge_possible, message
1080
1080
1081 def _has_largefiles(self, repo):
1081 def _has_largefiles(self, repo):
1082 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1082 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1083 'extensions', 'largefiles')
1083 'extensions', 'largefiles')
1084 return largefiles_ui and largefiles_ui[0].active
1084 return largefiles_ui and largefiles_ui[0].active
1085
1085
1086 def _try_merge(self, pull_request):
1086 def _try_merge(self, pull_request):
1087 """
1087 """
1088 Try to merge the pull request and return the merge status.
1088 Try to merge the pull request and return the merge status.
1089 """
1089 """
1090 log.debug(
1090 log.debug(
1091 "Trying out if the pull request %s can be merged.",
1091 "Trying out if the pull request %s can be merged.",
1092 pull_request.pull_request_id)
1092 pull_request.pull_request_id)
1093 target_vcs = pull_request.target_repo.scm_instance()
1093 target_vcs = pull_request.target_repo.scm_instance()
1094
1094
1095 # Refresh the target reference.
1095 # Refresh the target reference.
1096 try:
1096 try:
1097 target_ref = self._refresh_reference(
1097 target_ref = self._refresh_reference(
1098 pull_request.target_ref_parts, target_vcs)
1098 pull_request.target_ref_parts, target_vcs)
1099 except CommitDoesNotExistError:
1099 except CommitDoesNotExistError:
1100 merge_state = MergeResponse(
1100 merge_state = MergeResponse(
1101 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1101 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1102 return merge_state
1102 return merge_state
1103
1103
1104 target_locked = pull_request.target_repo.locked
1104 target_locked = pull_request.target_repo.locked
1105 if target_locked and target_locked[0]:
1105 if target_locked and target_locked[0]:
1106 log.debug("The target repository is locked.")
1106 log.debug("The target repository is locked.")
1107 merge_state = MergeResponse(
1107 merge_state = MergeResponse(
1108 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1108 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1109 elif self._needs_merge_state_refresh(pull_request, target_ref):
1109 elif self._needs_merge_state_refresh(pull_request, target_ref):
1110 log.debug("Refreshing the merge status of the repository.")
1110 log.debug("Refreshing the merge status of the repository.")
1111 merge_state = self._refresh_merge_state(
1111 merge_state = self._refresh_merge_state(
1112 pull_request, target_vcs, target_ref)
1112 pull_request, target_vcs, target_ref)
1113 else:
1113 else:
1114 possible = pull_request.\
1114 possible = pull_request.\
1115 _last_merge_status == MergeFailureReason.NONE
1115 _last_merge_status == MergeFailureReason.NONE
1116 merge_state = MergeResponse(
1116 merge_state = MergeResponse(
1117 possible, False, None, pull_request._last_merge_status)
1117 possible, False, None, pull_request._last_merge_status)
1118
1118
1119 return merge_state
1119 return merge_state
1120
1120
1121 def _refresh_reference(self, reference, vcs_repository):
1121 def _refresh_reference(self, reference, vcs_repository):
1122 if reference.type in ('branch', 'book'):
1122 if reference.type in ('branch', 'book'):
1123 name_or_id = reference.name
1123 name_or_id = reference.name
1124 else:
1124 else:
1125 name_or_id = reference.commit_id
1125 name_or_id = reference.commit_id
1126 refreshed_commit = vcs_repository.get_commit(name_or_id)
1126 refreshed_commit = vcs_repository.get_commit(name_or_id)
1127 refreshed_reference = Reference(
1127 refreshed_reference = Reference(
1128 reference.type, reference.name, refreshed_commit.raw_id)
1128 reference.type, reference.name, refreshed_commit.raw_id)
1129 return refreshed_reference
1129 return refreshed_reference
1130
1130
1131 def _needs_merge_state_refresh(self, pull_request, target_reference):
1131 def _needs_merge_state_refresh(self, pull_request, target_reference):
1132 return not(
1132 return not(
1133 pull_request.revisions and
1133 pull_request.revisions and
1134 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1134 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1135 target_reference.commit_id == pull_request._last_merge_target_rev)
1135 target_reference.commit_id == pull_request._last_merge_target_rev)
1136
1136
1137 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1137 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1138 workspace_id = self._workspace_id(pull_request)
1138 workspace_id = self._workspace_id(pull_request)
1139 source_vcs = pull_request.source_repo.scm_instance()
1139 source_vcs = pull_request.source_repo.scm_instance()
1140 use_rebase = self._use_rebase_for_merging(pull_request)
1140 use_rebase = self._use_rebase_for_merging(pull_request)
1141 merge_state = target_vcs.merge(
1141 merge_state = target_vcs.merge(
1142 target_reference, source_vcs, pull_request.source_ref_parts,
1142 target_reference, source_vcs, pull_request.source_ref_parts,
1143 workspace_id, dry_run=True, use_rebase=use_rebase)
1143 workspace_id, dry_run=True, use_rebase=use_rebase)
1144
1144
1145 # Do not store the response if there was an unknown error.
1145 # Do not store the response if there was an unknown error.
1146 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1146 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1147 pull_request._last_merge_source_rev = \
1147 pull_request._last_merge_source_rev = \
1148 pull_request.source_ref_parts.commit_id
1148 pull_request.source_ref_parts.commit_id
1149 pull_request._last_merge_target_rev = target_reference.commit_id
1149 pull_request._last_merge_target_rev = target_reference.commit_id
1150 pull_request._last_merge_status = merge_state.failure_reason
1150 pull_request._last_merge_status = merge_state.failure_reason
1151 pull_request.shadow_merge_ref = merge_state.merge_ref
1151 pull_request.shadow_merge_ref = merge_state.merge_ref
1152 Session().add(pull_request)
1152 Session().add(pull_request)
1153 Session().commit()
1153 Session().commit()
1154
1154
1155 return merge_state
1155 return merge_state
1156
1156
1157 def _workspace_id(self, pull_request):
1157 def _workspace_id(self, pull_request):
1158 workspace_id = 'pr-%s' % pull_request.pull_request_id
1158 workspace_id = 'pr-%s' % pull_request.pull_request_id
1159 return workspace_id
1159 return workspace_id
1160
1160
1161 def merge_status_message(self, status_code):
1161 def merge_status_message(self, status_code):
1162 """
1162 """
1163 Return a human friendly error message for the given merge status code.
1163 Return a human friendly error message for the given merge status code.
1164 """
1164 """
1165 return self.MERGE_STATUS_MESSAGES[status_code]
1165 return self.MERGE_STATUS_MESSAGES[status_code]
1166
1166
1167 def generate_repo_data(self, repo, commit_id=None, branch=None,
1167 def generate_repo_data(self, repo, commit_id=None, branch=None,
1168 bookmark=None):
1168 bookmark=None):
1169 all_refs, selected_ref = \
1169 all_refs, selected_ref = \
1170 self._get_repo_pullrequest_sources(
1170 self._get_repo_pullrequest_sources(
1171 repo.scm_instance(), commit_id=commit_id,
1171 repo.scm_instance(), commit_id=commit_id,
1172 branch=branch, bookmark=bookmark)
1172 branch=branch, bookmark=bookmark)
1173
1173
1174 refs_select2 = []
1174 refs_select2 = []
1175 for element in all_refs:
1175 for element in all_refs:
1176 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1176 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1177 refs_select2.append({'text': element[1], 'children': children})
1177 refs_select2.append({'text': element[1], 'children': children})
1178
1178
1179 return {
1179 return {
1180 'user': {
1180 'user': {
1181 'user_id': repo.user.user_id,
1181 'user_id': repo.user.user_id,
1182 'username': repo.user.username,
1182 'username': repo.user.username,
1183 'firstname': repo.user.firstname,
1183 'firstname': repo.user.firstname,
1184 'lastname': repo.user.lastname,
1184 'lastname': repo.user.lastname,
1185 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1185 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1186 },
1186 },
1187 'description': h.chop_at_smart(repo.description, '\n'),
1187 'description': h.chop_at_smart(repo.description, '\n'),
1188 'refs': {
1188 'refs': {
1189 'all_refs': all_refs,
1189 'all_refs': all_refs,
1190 'selected_ref': selected_ref,
1190 'selected_ref': selected_ref,
1191 'select2_refs': refs_select2
1191 'select2_refs': refs_select2
1192 }
1192 }
1193 }
1193 }
1194
1194
1195 def generate_pullrequest_title(self, source, source_ref, target):
1195 def generate_pullrequest_title(self, source, source_ref, target):
1196 return u'{source}#{at_ref} to {target}'.format(
1196 return u'{source}#{at_ref} to {target}'.format(
1197 source=source,
1197 source=source,
1198 at_ref=source_ref,
1198 at_ref=source_ref,
1199 target=target,
1199 target=target,
1200 )
1200 )
1201
1201
1202 def _cleanup_merge_workspace(self, pull_request):
1202 def _cleanup_merge_workspace(self, pull_request):
1203 # Merging related cleanup
1203 # Merging related cleanup
1204 target_scm = pull_request.target_repo.scm_instance()
1204 target_scm = pull_request.target_repo.scm_instance()
1205 workspace_id = 'pr-%s' % pull_request.pull_request_id
1205 workspace_id = 'pr-%s' % pull_request.pull_request_id
1206
1206
1207 try:
1207 try:
1208 target_scm.cleanup_merge_workspace(workspace_id)
1208 target_scm.cleanup_merge_workspace(workspace_id)
1209 except NotImplementedError:
1209 except NotImplementedError:
1210 pass
1210 pass
1211
1211
1212 def _get_repo_pullrequest_sources(
1212 def _get_repo_pullrequest_sources(
1213 self, repo, commit_id=None, branch=None, bookmark=None):
1213 self, repo, commit_id=None, branch=None, bookmark=None):
1214 """
1214 """
1215 Return a structure with repo's interesting commits, suitable for
1215 Return a structure with repo's interesting commits, suitable for
1216 the selectors in pullrequest controller
1216 the selectors in pullrequest controller
1217
1217
1218 :param commit_id: a commit that must be in the list somehow
1218 :param commit_id: a commit that must be in the list somehow
1219 and selected by default
1219 and selected by default
1220 :param branch: a branch that must be in the list and selected
1220 :param branch: a branch that must be in the list and selected
1221 by default - even if closed
1221 by default - even if closed
1222 :param bookmark: a bookmark that must be in the list and selected
1222 :param bookmark: a bookmark that must be in the list and selected
1223 """
1223 """
1224
1224
1225 commit_id = safe_str(commit_id) if commit_id else None
1225 commit_id = safe_str(commit_id) if commit_id else None
1226 branch = safe_str(branch) if branch else None
1226 branch = safe_str(branch) if branch else None
1227 bookmark = safe_str(bookmark) if bookmark else None
1227 bookmark = safe_str(bookmark) if bookmark else None
1228
1228
1229 selected = None
1229 selected = None
1230
1230
1231 # order matters: first source that has commit_id in it will be selected
1231 # order matters: first source that has commit_id in it will be selected
1232 sources = []
1232 sources = []
1233 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1233 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1234 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1234 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1235
1235
1236 if commit_id:
1236 if commit_id:
1237 ref_commit = (h.short_id(commit_id), commit_id)
1237 ref_commit = (h.short_id(commit_id), commit_id)
1238 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1238 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1239
1239
1240 sources.append(
1240 sources.append(
1241 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1241 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1242 )
1242 )
1243
1243
1244 groups = []
1244 groups = []
1245 for group_key, ref_list, group_name, match in sources:
1245 for group_key, ref_list, group_name, match in sources:
1246 group_refs = []
1246 group_refs = []
1247 for ref_name, ref_id in ref_list:
1247 for ref_name, ref_id in ref_list:
1248 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1248 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1249 group_refs.append((ref_key, ref_name))
1249 group_refs.append((ref_key, ref_name))
1250
1250
1251 if not selected:
1251 if not selected:
1252 if set([commit_id, match]) & set([ref_id, ref_name]):
1252 if set([commit_id, match]) & set([ref_id, ref_name]):
1253 selected = ref_key
1253 selected = ref_key
1254
1254
1255 if group_refs:
1255 if group_refs:
1256 groups.append((group_refs, group_name))
1256 groups.append((group_refs, group_name))
1257
1257
1258 if not selected:
1258 if not selected:
1259 ref = commit_id or branch or bookmark
1259 ref = commit_id or branch or bookmark
1260 if ref:
1260 if ref:
1261 raise CommitDoesNotExistError(
1261 raise CommitDoesNotExistError(
1262 'No commit refs could be found matching: %s' % ref)
1262 'No commit refs could be found matching: %s' % ref)
1263 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1263 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1264 selected = 'branch:%s:%s' % (
1264 selected = 'branch:%s:%s' % (
1265 repo.DEFAULT_BRANCH_NAME,
1265 repo.DEFAULT_BRANCH_NAME,
1266 repo.branches[repo.DEFAULT_BRANCH_NAME]
1266 repo.branches[repo.DEFAULT_BRANCH_NAME]
1267 )
1267 )
1268 elif repo.commit_ids:
1268 elif repo.commit_ids:
1269 rev = repo.commit_ids[0]
1269 rev = repo.commit_ids[0]
1270 selected = 'rev:%s:%s' % (rev, rev)
1270 selected = 'rev:%s:%s' % (rev, rev)
1271 else:
1271 else:
1272 raise EmptyRepositoryError()
1272 raise EmptyRepositoryError()
1273 return groups, selected
1273 return groups, selected
1274
1274
1275 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1275 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1276 return self._get_diff_from_pr_or_version(
1276 return self._get_diff_from_pr_or_version(
1277 source_repo, source_ref_id, target_ref_id, context=context)
1277 source_repo, source_ref_id, target_ref_id, context=context)
1278
1278
1279 def _get_diff_from_pr_or_version(
1279 def _get_diff_from_pr_or_version(
1280 self, source_repo, source_ref_id, target_ref_id, context):
1280 self, source_repo, source_ref_id, target_ref_id, context):
1281 target_commit = source_repo.get_commit(
1281 target_commit = source_repo.get_commit(
1282 commit_id=safe_str(target_ref_id))
1282 commit_id=safe_str(target_ref_id))
1283 source_commit = source_repo.get_commit(
1283 source_commit = source_repo.get_commit(
1284 commit_id=safe_str(source_ref_id))
1284 commit_id=safe_str(source_ref_id))
1285 if isinstance(source_repo, Repository):
1285 if isinstance(source_repo, Repository):
1286 vcs_repo = source_repo.scm_instance()
1286 vcs_repo = source_repo.scm_instance()
1287 else:
1287 else:
1288 vcs_repo = source_repo
1288 vcs_repo = source_repo
1289
1289
1290 # TODO: johbo: In the context of an update, we cannot reach
1290 # TODO: johbo: In the context of an update, we cannot reach
1291 # the old commit anymore with our normal mechanisms. It needs
1291 # the old commit anymore with our normal mechanisms. It needs
1292 # some sort of special support in the vcs layer to avoid this
1292 # some sort of special support in the vcs layer to avoid this
1293 # workaround.
1293 # workaround.
1294 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1294 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1295 vcs_repo.alias == 'git'):
1295 vcs_repo.alias == 'git'):
1296 source_commit.raw_id = safe_str(source_ref_id)
1296 source_commit.raw_id = safe_str(source_ref_id)
1297
1297
1298 log.debug('calculating diff between '
1298 log.debug('calculating diff between '
1299 'source_ref:%s and target_ref:%s for repo `%s`',
1299 'source_ref:%s and target_ref:%s for repo `%s`',
1300 target_ref_id, source_ref_id,
1300 target_ref_id, source_ref_id,
1301 safe_unicode(vcs_repo.path))
1301 safe_unicode(vcs_repo.path))
1302
1302
1303 vcs_diff = vcs_repo.get_diff(
1303 vcs_diff = vcs_repo.get_diff(
1304 commit1=target_commit, commit2=source_commit, context=context)
1304 commit1=target_commit, commit2=source_commit, context=context)
1305 return vcs_diff
1305 return vcs_diff
1306
1306
1307 def _is_merge_enabled(self, pull_request):
1307 def _is_merge_enabled(self, pull_request):
1308 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1308 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1309 settings = settings_model.get_general_settings()
1309 settings = settings_model.get_general_settings()
1310 return settings.get('rhodecode_pr_merge_enabled', False)
1310 return settings.get('rhodecode_pr_merge_enabled', False)
1311
1311
1312 def _use_rebase_for_merging(self, pull_request):
1312 def _use_rebase_for_merging(self, pull_request):
1313 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1313 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1314 settings = settings_model.get_general_settings()
1314 settings = settings_model.get_general_settings()
1315 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1315 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1316
1316
1317 def _log_action(self, action, user, pull_request):
1317 def _log_action(self, action, user, pull_request):
1318 action_logger(
1318 action_logger(
1319 user,
1319 user,
1320 '{action}:{pr_id}'.format(
1320 '{action}:{pr_id}'.format(
1321 action=action, pr_id=pull_request.pull_request_id),
1321 action=action, pr_id=pull_request.pull_request_id),
1322 pull_request.target_repo)
1322 pull_request.target_repo)
1323
1323
1324
1324
1325 class MergeCheck(object):
1325 class MergeCheck(object):
1326 """
1326 """
1327 Perform Merge Checks and returns a check object which stores information
1327 Perform Merge Checks and returns a check object which stores information
1328 about merge errors, and merge conditions
1328 about merge errors, and merge conditions
1329 """
1329 """
1330 TODO_CHECK = 'todo'
1330 TODO_CHECK = 'todo'
1331 PERM_CHECK = 'perm'
1331 PERM_CHECK = 'perm'
1332 REVIEW_CHECK = 'review'
1332 REVIEW_CHECK = 'review'
1333 MERGE_CHECK = 'merge'
1333 MERGE_CHECK = 'merge'
1334
1334
1335 def __init__(self):
1335 def __init__(self):
1336 self.review_status = None
1336 self.merge_possible = None
1337 self.merge_possible = None
1337 self.merge_msg = ''
1338 self.merge_msg = ''
1338 self.failed = None
1339 self.failed = None
1339 self.errors = []
1340 self.errors = []
1340 self.error_details = OrderedDict()
1341 self.error_details = OrderedDict()
1341
1342
1342 def push_error(self, error_type, message, error_key, details):
1343 def push_error(self, error_type, message, error_key, details):
1343 self.failed = True
1344 self.failed = True
1344 self.errors.append([error_type, message])
1345 self.errors.append([error_type, message])
1345 self.error_details[error_key] = dict(
1346 self.error_details[error_key] = dict(
1346 details=details,
1347 details=details,
1347 error_type=error_type,
1348 error_type=error_type,
1348 message=message
1349 message=message
1349 )
1350 )
1350
1351
1351 @classmethod
1352 @classmethod
1352 def validate(cls, pull_request, user, fail_early=False, translator=None):
1353 def validate(cls, pull_request, user, fail_early=False, translator=None):
1353 # if migrated to pyramid...
1354 # if migrated to pyramid...
1354 # _ = lambda: translator or _ # use passed in translator if any
1355 # _ = lambda: translator or _ # use passed in translator if any
1355
1356
1356 merge_check = cls()
1357 merge_check = cls()
1357
1358
1358 # permissions
1359 # permissions to merge
1359 user_allowed_to_merge = PullRequestModel().check_user_merge(
1360 user_allowed_to_merge = PullRequestModel().check_user_merge(
1360 pull_request, user)
1361 pull_request, user)
1361 if not user_allowed_to_merge:
1362 if not user_allowed_to_merge:
1362 log.debug("MergeCheck: cannot merge, approval is pending.")
1363 log.debug("MergeCheck: cannot merge, approval is pending.")
1363
1364
1364 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1365 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1365 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1366 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1366 if fail_early:
1367 if fail_early:
1367 return merge_check
1368 return merge_check
1368
1369
1369 # review status
1370 # review status, must be always present
1370 review_status = pull_request.calculated_review_status()
1371 review_status = pull_request.calculated_review_status()
1372 merge_check.review_status = review_status
1373
1371 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1374 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1372 if not status_approved:
1375 if not status_approved:
1373 log.debug("MergeCheck: cannot merge, approval is pending.")
1376 log.debug("MergeCheck: cannot merge, approval is pending.")
1374
1377
1375 msg = _('Pull request reviewer approval is pending.')
1378 msg = _('Pull request reviewer approval is pending.')
1376
1379
1377 merge_check.push_error(
1380 merge_check.push_error(
1378 'warning', msg, cls.REVIEW_CHECK, review_status)
1381 'warning', msg, cls.REVIEW_CHECK, review_status)
1379
1382
1380 if fail_early:
1383 if fail_early:
1381 return merge_check
1384 return merge_check
1382
1385
1383 # left over TODOs
1386 # left over TODOs
1384 todos = CommentsModel().get_unresolved_todos(pull_request)
1387 todos = CommentsModel().get_unresolved_todos(pull_request)
1385 if todos:
1388 if todos:
1386 log.debug("MergeCheck: cannot merge, {} "
1389 log.debug("MergeCheck: cannot merge, {} "
1387 "unresolved todos left.".format(len(todos)))
1390 "unresolved todos left.".format(len(todos)))
1388
1391
1389 if len(todos) == 1:
1392 if len(todos) == 1:
1390 msg = _('Cannot merge, {} TODO still not resolved.').format(
1393 msg = _('Cannot merge, {} TODO still not resolved.').format(
1391 len(todos))
1394 len(todos))
1392 else:
1395 else:
1393 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1396 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1394 len(todos))
1397 len(todos))
1395
1398
1396 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1399 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1397
1400
1398 if fail_early:
1401 if fail_early:
1399 return merge_check
1402 return merge_check
1400
1403
1401 # merge possible
1404 # merge possible
1402 merge_status, msg = PullRequestModel().merge_status(pull_request)
1405 merge_status, msg = PullRequestModel().merge_status(pull_request)
1403 merge_check.merge_possible = merge_status
1406 merge_check.merge_possible = merge_status
1404 merge_check.merge_msg = msg
1407 merge_check.merge_msg = msg
1405 if not merge_status:
1408 if not merge_status:
1406 log.debug(
1409 log.debug(
1407 "MergeCheck: cannot merge, pull request merge not possible.")
1410 "MergeCheck: cannot merge, pull request merge not possible.")
1408 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1411 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1409
1412
1410 if fail_early:
1413 if fail_early:
1411 return merge_check
1414 return merge_check
1412
1415
1413 return merge_check
1416 return merge_check
1414
1417
1415
1418
1416 ChangeTuple = namedtuple('ChangeTuple',
1419 ChangeTuple = namedtuple('ChangeTuple',
1417 ['added', 'common', 'removed', 'total'])
1420 ['added', 'common', 'removed', 'total'])
1418
1421
1419 FileChangeTuple = namedtuple('FileChangeTuple',
1422 FileChangeTuple = namedtuple('FileChangeTuple',
1420 ['added', 'modified', 'removed'])
1423 ['added', 'modified', 'removed'])
@@ -1,395 +1,408 b''
1
1
2
2
3 //BUTTONS
3 //BUTTONS
4 button,
4 button,
5 .btn,
5 .btn,
6 input[type="button"] {
6 input[type="button"] {
7 -webkit-appearance: none;
7 -webkit-appearance: none;
8 display: inline-block;
8 display: inline-block;
9 margin: 0 @padding/3 0 0;
9 margin: 0 @padding/3 0 0;
10 padding: @button-padding;
10 padding: @button-padding;
11 text-align: center;
11 text-align: center;
12 font-size: @basefontsize;
12 font-size: @basefontsize;
13 line-height: 1em;
13 line-height: 1em;
14 font-family: @text-light;
14 font-family: @text-light;
15 text-decoration: none;
15 text-decoration: none;
16 text-shadow: none;
16 text-shadow: none;
17 color: @grey4;
17 color: @grey4;
18 background-color: white;
18 background-color: white;
19 background-image: none;
19 background-image: none;
20 border: none;
20 border: none;
21 .border ( @border-thickness-buttons, @grey4 );
21 .border ( @border-thickness-buttons, @grey4 );
22 .border-radius (@border-radius);
22 .border-radius (@border-radius);
23 cursor: pointer;
23 cursor: pointer;
24 white-space: nowrap;
24 white-space: nowrap;
25 -webkit-transition: background .3s,color .3s;
25 -webkit-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
29
29
30 a {
30 a {
31 display: block;
31 display: block;
32 margin: 0;
32 margin: 0;
33 padding: 0;
33 padding: 0;
34 color: inherit;
34 color: inherit;
35 text-decoration: none;
35 text-decoration: none;
36
36
37 &:hover {
37 &:hover {
38 text-decoration: none;
38 text-decoration: none;
39 }
39 }
40 }
40 }
41
41
42 &:focus,
42 &:focus,
43 &:active {
43 &:active {
44 outline:none;
44 outline:none;
45 }
45 }
46 &:hover {
46 &:hover {
47 color: white;
47 color: white;
48 background-color: @grey4;
48 background-color: @grey4;
49 }
49 }
50
50
51 .icon-remove-sign {
51 .icon-remove-sign {
52 display: none;
52 display: none;
53 }
53 }
54
54
55 //disabled buttons
55 //disabled buttons
56 //last; overrides any other styles
56 //last; overrides any other styles
57 &:disabled {
57 &:disabled {
58 opacity: .7;
58 opacity: .7;
59 cursor: auto;
59 cursor: auto;
60 background-color: white;
60 background-color: white;
61 color: @grey4;
61 color: @grey4;
62 text-shadow: none;
62 text-shadow: none;
63 }
63 }
64
64
65 }
65 }
66
66
67
67
68 .btn-default {
68 .btn-default {
69 .border ( @border-thickness-buttons, @rcblue );
69 .border ( @border-thickness-buttons, @rcblue );
70 background-image: none;
70 background-image: none;
71 color: @rcblue;
71 color: @rcblue;
72
72
73 a {
73 a {
74 color: @rcblue;
74 color: @rcblue;
75 }
75 }
76
76
77 &:hover,
77 &:hover,
78 &.active {
78 &.active {
79 color: white;
79 color: white;
80 background-color: @rcdarkblue;
80 background-color: @rcdarkblue;
81 .border ( @border-thickness, @rcdarkblue );
81 .border ( @border-thickness, @rcdarkblue );
82
82
83 a {
83 a {
84 color: white;
84 color: white;
85 }
85 }
86 }
86 }
87 &:disabled {
87 &:disabled {
88 .border ( @border-thickness-buttons, @grey4 );
88 .border ( @border-thickness-buttons, @grey4 );
89 background-color: transparent;
89 background-color: transparent;
90 }
90 }
91 }
91 }
92
92
93 .btn-primary,
93 .btn-primary,
94 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
94 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
95 .btn-success {
95 .btn-success {
96 .border ( @border-thickness, @rcblue );
96 .border ( @border-thickness, @rcblue );
97 background-color: @rcblue;
97 background-color: @rcblue;
98 color: white;
98 color: white;
99
99
100 a {
100 a {
101 color: white;
101 color: white;
102 }
102 }
103
103
104 &:hover,
104 &:hover,
105 &.active {
105 &.active {
106 .border ( @border-thickness, @rcdarkblue );
106 .border ( @border-thickness, @rcdarkblue );
107 color: white;
107 color: white;
108 background-color: @rcdarkblue;
108 background-color: @rcdarkblue;
109
109
110 a {
110 a {
111 color: white;
111 color: white;
112 }
112 }
113 }
113 }
114 &:disabled {
114 &:disabled {
115 background-color: @rcblue;
115 background-color: @rcblue;
116 }
116 }
117 }
117 }
118
118
119 .btn-secondary {
119 .btn-secondary {
120 &:extend(.btn-default);
120 &:extend(.btn-default);
121
121
122 background-color: white;
122 background-color: white;
123
123
124 &:focus {
124 &:focus {
125 outline: 0;
125 outline: 0;
126 }
126 }
127
127
128 &:hover {
128 &:hover {
129 &:extend(.btn-default:hover);
129 &:extend(.btn-default:hover);
130 }
130 }
131
131
132 &.btn-link {
132 &.btn-link {
133 &:extend(.btn-link);
133 &:extend(.btn-link);
134 color: @rcblue;
134 color: @rcblue;
135 }
135 }
136
136
137 &:disabled {
137 &:disabled {
138 color: @rcblue;
138 color: @rcblue;
139 background-color: white;
139 background-color: white;
140 }
140 }
141 }
141 }
142
142
143 .btn-warning,
143 .btn-warning,
144 .btn-danger,
144 .btn-danger,
145 .revoke_perm,
145 .revoke_perm,
146 .btn-x,
146 .btn-x,
147 .form .action_button.btn-x {
147 .form .action_button.btn-x {
148 .border ( @border-thickness, @alert2 );
148 .border ( @border-thickness, @alert2 );
149 background-color: white;
149 background-color: white;
150 color: @alert2;
150 color: @alert2;
151
151
152 a {
152 a {
153 color: @alert2;
153 color: @alert2;
154 }
154 }
155
155
156 &:hover,
156 &:hover,
157 &.active {
157 &.active {
158 .border ( @border-thickness, @alert2 );
158 .border ( @border-thickness, @alert2 );
159 color: white;
159 color: white;
160 background-color: @alert2;
160 background-color: @alert2;
161
161
162 a {
162 a {
163 color: white;
163 color: white;
164 }
164 }
165 }
165 }
166
166
167 i {
167 i {
168 display:none;
168 display:none;
169 }
169 }
170
170
171 &:disabled {
171 &:disabled {
172 background-color: white;
172 background-color: white;
173 color: @alert2;
173 color: @alert2;
174 }
174 }
175 }
175 }
176
176
177 .btn-approved-status {
178 .border ( @border-thickness, @alert1 );
179 background-color: white;
180 color: @alert1;
181
182 }
183
184 .btn-rejected-status {
185 .border ( @border-thickness, @alert2 );
186 background-color: white;
187 color: @alert2;
188 }
189
177 .btn-sm,
190 .btn-sm,
178 .btn-mini,
191 .btn-mini,
179 .field-sm .btn {
192 .field-sm .btn {
180 padding: @padding/3;
193 padding: @padding/3;
181 }
194 }
182
195
183 .btn-xs {
196 .btn-xs {
184 padding: @padding/4;
197 padding: @padding/4;
185 }
198 }
186
199
187 .btn-lg {
200 .btn-lg {
188 padding: @padding * 1.2;
201 padding: @padding * 1.2;
189 }
202 }
190
203
191 .btn-group {
204 .btn-group {
192 display: inline-block;
205 display: inline-block;
193 .btn {
206 .btn {
194 float: left;
207 float: left;
195 margin: 0 0 0 -1px;
208 margin: 0 0 0 -1px;
196 }
209 }
197 }
210 }
198
211
199 .btn-link {
212 .btn-link {
200 background: transparent;
213 background: transparent;
201 border: none;
214 border: none;
202 padding: 0;
215 padding: 0;
203 color: @rcblue;
216 color: @rcblue;
204
217
205 &:hover {
218 &:hover {
206 background: transparent;
219 background: transparent;
207 border: none;
220 border: none;
208 color: @rcdarkblue;
221 color: @rcdarkblue;
209 }
222 }
210
223
211 //disabled buttons
224 //disabled buttons
212 //last; overrides any other styles
225 //last; overrides any other styles
213 &:disabled {
226 &:disabled {
214 opacity: .7;
227 opacity: .7;
215 cursor: auto;
228 cursor: auto;
216 background-color: white;
229 background-color: white;
217 color: @grey4;
230 color: @grey4;
218 text-shadow: none;
231 text-shadow: none;
219 }
232 }
220
233
221 // TODO: johbo: Check if we can avoid this, indicates that the structure
234 // TODO: johbo: Check if we can avoid this, indicates that the structure
222 // is not yet good.
235 // is not yet good.
223 // lisa: The button CSS reflects the button HTML; both need a cleanup.
236 // lisa: The button CSS reflects the button HTML; both need a cleanup.
224 &.btn-danger {
237 &.btn-danger {
225 color: @alert2;
238 color: @alert2;
226
239
227 &:hover {
240 &:hover {
228 color: darken(@alert2,30%);
241 color: darken(@alert2,30%);
229 }
242 }
230
243
231 &:disabled {
244 &:disabled {
232 color: @alert2;
245 color: @alert2;
233 }
246 }
234 }
247 }
235 }
248 }
236
249
237 .btn-social {
250 .btn-social {
238 &:extend(.btn-default);
251 &:extend(.btn-default);
239 margin: 5px 5px 5px 0px;
252 margin: 5px 5px 5px 0px;
240 min-width: 150px;
253 min-width: 150px;
241 }
254 }
242
255
243 // TODO: johbo: check these exceptions
256 // TODO: johbo: check these exceptions
244
257
245 .links {
258 .links {
246
259
247 .btn + .btn {
260 .btn + .btn {
248 margin-top: @padding;
261 margin-top: @padding;
249 }
262 }
250 }
263 }
251
264
252
265
253 .action_button {
266 .action_button {
254 display:inline;
267 display:inline;
255 margin: 0;
268 margin: 0;
256 padding: 0 1em 0 0;
269 padding: 0 1em 0 0;
257 font-size: inherit;
270 font-size: inherit;
258 color: @rcblue;
271 color: @rcblue;
259 border: none;
272 border: none;
260 .border-radius (0);
273 .border-radius (0);
261 background-color: transparent;
274 background-color: transparent;
262
275
263 &:last-child {
276 &:last-child {
264 border: none;
277 border: none;
265 }
278 }
266
279
267 &:hover {
280 &:hover {
268 color: @rcdarkblue;
281 color: @rcdarkblue;
269 background-color: transparent;
282 background-color: transparent;
270 border: none;
283 border: none;
271 }
284 }
272 }
285 }
273 .grid_delete {
286 .grid_delete {
274 .action_button {
287 .action_button {
275 border: none;
288 border: none;
276 }
289 }
277 }
290 }
278
291
279
292
280 // TODO: johbo: Form button tweaks, check if we can use the classes instead
293 // TODO: johbo: Form button tweaks, check if we can use the classes instead
281 input[type="submit"] {
294 input[type="submit"] {
282 &:extend(.btn-primary);
295 &:extend(.btn-primary);
283
296
284 &:focus {
297 &:focus {
285 outline: 0;
298 outline: 0;
286 }
299 }
287
300
288 &:hover {
301 &:hover {
289 &:extend(.btn-primary:hover);
302 &:extend(.btn-primary:hover);
290 }
303 }
291
304
292 &.btn-link {
305 &.btn-link {
293 &:extend(.btn-link);
306 &:extend(.btn-link);
294 color: @rcblue;
307 color: @rcblue;
295
308
296 &:disabled {
309 &:disabled {
297 color: @rcblue;
310 color: @rcblue;
298 background-color: transparent;
311 background-color: transparent;
299 }
312 }
300 }
313 }
301
314
302 &:disabled {
315 &:disabled {
303 .border ( @border-thickness-buttons, @rcblue );
316 .border ( @border-thickness-buttons, @rcblue );
304 background-color: @rcblue;
317 background-color: @rcblue;
305 color: white;
318 color: white;
306 }
319 }
307 }
320 }
308
321
309 input[type="reset"] {
322 input[type="reset"] {
310 &:extend(.btn-default);
323 &:extend(.btn-default);
311
324
312 // TODO: johbo: Check if this tweak can be avoided.
325 // TODO: johbo: Check if this tweak can be avoided.
313 background: transparent;
326 background: transparent;
314
327
315 &:focus {
328 &:focus {
316 outline: 0;
329 outline: 0;
317 }
330 }
318
331
319 &:hover {
332 &:hover {
320 &:extend(.btn-default:hover);
333 &:extend(.btn-default:hover);
321 }
334 }
322
335
323 &.btn-link {
336 &.btn-link {
324 &:extend(.btn-link);
337 &:extend(.btn-link);
325 color: @rcblue;
338 color: @rcblue;
326
339
327 &:disabled {
340 &:disabled {
328 border: none;
341 border: none;
329 }
342 }
330 }
343 }
331
344
332 &:disabled {
345 &:disabled {
333 .border ( @border-thickness-buttons, @rcblue );
346 .border ( @border-thickness-buttons, @rcblue );
334 background-color: white;
347 background-color: white;
335 color: @rcblue;
348 color: @rcblue;
336 }
349 }
337 }
350 }
338
351
339 input[type="submit"],
352 input[type="submit"],
340 input[type="reset"] {
353 input[type="reset"] {
341 &.btn-danger {
354 &.btn-danger {
342 &:extend(.btn-danger);
355 &:extend(.btn-danger);
343
356
344 &:focus {
357 &:focus {
345 outline: 0;
358 outline: 0;
346 }
359 }
347
360
348 &:hover {
361 &:hover {
349 &:extend(.btn-danger:hover);
362 &:extend(.btn-danger:hover);
350 }
363 }
351
364
352 &.btn-link {
365 &.btn-link {
353 &:extend(.btn-link);
366 &:extend(.btn-link);
354 color: @alert2;
367 color: @alert2;
355
368
356 &:hover {
369 &:hover {
357 color: darken(@alert2,30%);
370 color: darken(@alert2,30%);
358 }
371 }
359 }
372 }
360
373
361 &:disabled {
374 &:disabled {
362 color: @alert2;
375 color: @alert2;
363 background-color: white;
376 background-color: white;
364 }
377 }
365 }
378 }
366 &.btn-danger-action {
379 &.btn-danger-action {
367 .border ( @border-thickness, @alert2 );
380 .border ( @border-thickness, @alert2 );
368 background-color: @alert2;
381 background-color: @alert2;
369 color: white;
382 color: white;
370
383
371 a {
384 a {
372 color: white;
385 color: white;
373 }
386 }
374
387
375 &:hover {
388 &:hover {
376 background-color: darken(@alert2,20%);
389 background-color: darken(@alert2,20%);
377 }
390 }
378
391
379 &.active {
392 &.active {
380 .border ( @border-thickness, @alert2 );
393 .border ( @border-thickness, @alert2 );
381 color: white;
394 color: white;
382 background-color: @alert2;
395 background-color: @alert2;
383
396
384 a {
397 a {
385 color: white;
398 color: white;
386 }
399 }
387 }
400 }
388
401
389 &:disabled {
402 &:disabled {
390 background-color: white;
403 background-color: white;
391 color: @alert2;
404 color: @alert2;
392 }
405 }
393 }
406 }
394 }
407 }
395
408
@@ -1,571 +1,575 b''
1 // comments.less
1 // comments.less
2 // For use in RhodeCode applications;
2 // For use in RhodeCode applications;
3 // see style guide documentation for guidelines.
3 // see style guide documentation for guidelines.
4
4
5
5
6 // Comments
6 // Comments
7 @comment-outdated-opacity: 0.6;
7 @comment-outdated-opacity: 0.6;
8
8
9 .comments {
9 .comments {
10 width: 100%;
10 width: 100%;
11 }
11 }
12
12
13 tr.inline-comments div {
13 tr.inline-comments div {
14 max-width: 100%;
14 max-width: 100%;
15
15
16 p {
16 p {
17 white-space: normal;
17 white-space: normal;
18 }
18 }
19
19
20 code, pre, .code, dd {
20 code, pre, .code, dd {
21 overflow-x: auto;
21 overflow-x: auto;
22 width: 1062px;
22 width: 1062px;
23 }
23 }
24
24
25 dd {
25 dd {
26 width: auto;
26 width: auto;
27 }
27 }
28 }
28 }
29
29
30 #injected_page_comments {
30 #injected_page_comments {
31 .comment-previous-link,
31 .comment-previous-link,
32 .comment-next-link,
32 .comment-next-link,
33 .comment-links-divider {
33 .comment-links-divider {
34 display: none;
34 display: none;
35 }
35 }
36 }
36 }
37
37
38 .add-comment {
38 .add-comment {
39 margin-bottom: 10px;
39 margin-bottom: 10px;
40 }
40 }
41 .hide-comment-button .add-comment {
41 .hide-comment-button .add-comment {
42 display: none;
42 display: none;
43 }
43 }
44
44
45 .comment-bubble {
45 .comment-bubble {
46 color: @grey4;
46 color: @grey4;
47 margin-top: 4px;
47 margin-top: 4px;
48 margin-right: 30px;
48 margin-right: 30px;
49 visibility: hidden;
49 visibility: hidden;
50 }
50 }
51
51
52 .comment-label {
52 .comment-label {
53 float: left;
53 float: left;
54
54
55 padding: 0.4em 0.4em;
55 padding: 0.4em 0.4em;
56 margin: 3px 5px 0px -10px;
56 margin: 3px 5px 0px -10px;
57 display: inline-block;
57 display: inline-block;
58 min-height: 0;
58 min-height: 0;
59
59
60 text-align: center;
60 text-align: center;
61 font-size: 10px;
61 font-size: 10px;
62 line-height: .8em;
62 line-height: .8em;
63
63
64 font-family: @text-italic;
64 font-family: @text-italic;
65 background: #fff none;
65 background: #fff none;
66 color: @grey4;
66 color: @grey4;
67 border: 1px solid @grey4;
67 border: 1px solid @grey4;
68 white-space: nowrap;
68 white-space: nowrap;
69
69
70 text-transform: uppercase;
70 text-transform: uppercase;
71 min-width: 40px;
71 min-width: 40px;
72
72
73 &.todo {
73 &.todo {
74 color: @color5;
74 color: @color5;
75 font-family: @text-bold-italic;
75 font-family: @text-bold-italic;
76 }
76 }
77
77
78 .resolve {
78 .resolve {
79 cursor: pointer;
79 cursor: pointer;
80 text-decoration: underline;
80 text-decoration: underline;
81 }
81 }
82
82
83 .resolved {
83 .resolved {
84 text-decoration: line-through;
84 text-decoration: line-through;
85 color: @color1;
85 color: @color1;
86 }
86 }
87 .resolved a {
87 .resolved a {
88 text-decoration: line-through;
88 text-decoration: line-through;
89 color: @color1;
89 color: @color1;
90 }
90 }
91 .resolve-text {
91 .resolve-text {
92 color: @color1;
92 color: @color1;
93 margin: 2px 8px;
93 margin: 2px 8px;
94 font-family: @text-italic;
94 font-family: @text-italic;
95 }
95 }
96 }
96 }
97
97
98 .has-spacer-after {
98 .has-spacer-after {
99 &:after {
99 &:after {
100 content: ' | ';
100 content: ' | ';
101 color: @grey5;
101 color: @grey5;
102 }
102 }
103 }
103 }
104
104
105 .has-spacer-before {
105 .has-spacer-before {
106 &:before {
106 &:before {
107 content: ' | ';
107 content: ' | ';
108 color: @grey5;
108 color: @grey5;
109 }
109 }
110 }
110 }
111
111
112 .comment {
112 .comment {
113
113
114 &.comment-general {
114 &.comment-general {
115 border: 1px solid @grey5;
115 border: 1px solid @grey5;
116 padding: 5px 5px 5px 5px;
116 padding: 5px 5px 5px 5px;
117 }
117 }
118
118
119 margin: @padding 0;
119 margin: @padding 0;
120 padding: 4px 0 0 0;
120 padding: 4px 0 0 0;
121 line-height: 1em;
121 line-height: 1em;
122
122
123 .rc-user {
123 .rc-user {
124 min-width: 0;
124 min-width: 0;
125 margin: 0px .5em 0 0;
125 margin: 0px .5em 0 0;
126
126
127 .user {
127 .user {
128 display: inline;
128 display: inline;
129 }
129 }
130 }
130 }
131
131
132 .meta {
132 .meta {
133 position: relative;
133 position: relative;
134 width: 100%;
134 width: 100%;
135 border-bottom: 1px solid @grey5;
135 border-bottom: 1px solid @grey5;
136 margin: -5px 0px;
136 margin: -5px 0px;
137 line-height: 24px;
137 line-height: 24px;
138
138
139 &:hover .permalink {
139 &:hover .permalink {
140 visibility: visible;
140 visibility: visible;
141 color: @rcblue;
141 color: @rcblue;
142 }
142 }
143 }
143 }
144
144
145 .author,
145 .author,
146 .date {
146 .date {
147 display: inline;
147 display: inline;
148
148
149 &:after {
149 &:after {
150 content: ' | ';
150 content: ' | ';
151 color: @grey5;
151 color: @grey5;
152 }
152 }
153 }
153 }
154
154
155 .author-general img {
155 .author-general img {
156 top: 3px;
156 top: 3px;
157 }
157 }
158 .author-inline img {
158 .author-inline img {
159 top: 3px;
159 top: 3px;
160 }
160 }
161
161
162 .status-change,
162 .status-change,
163 .permalink,
163 .permalink,
164 .changeset-status-lbl {
164 .changeset-status-lbl {
165 display: inline;
165 display: inline;
166 }
166 }
167
167
168 .permalink {
168 .permalink {
169 visibility: hidden;
169 visibility: hidden;
170 }
170 }
171
171
172 .comment-links-divider {
172 .comment-links-divider {
173 display: inline;
173 display: inline;
174 }
174 }
175
175
176 .comment-links-block {
176 .comment-links-block {
177 float:right;
177 float:right;
178 text-align: right;
178 text-align: right;
179 min-width: 85px;
179 min-width: 85px;
180
180
181 [class^="icon-"]:before,
181 [class^="icon-"]:before,
182 [class*=" icon-"]:before {
182 [class*=" icon-"]:before {
183 margin-left: 0;
183 margin-left: 0;
184 margin-right: 0;
184 margin-right: 0;
185 }
185 }
186 }
186 }
187
187
188 .comment-previous-link {
188 .comment-previous-link {
189 display: inline-block;
189 display: inline-block;
190
190
191 .arrow_comment_link{
191 .arrow_comment_link{
192 cursor: pointer;
192 cursor: pointer;
193 i {
193 i {
194 font-size:10px;
194 font-size:10px;
195 }
195 }
196 }
196 }
197 .arrow_comment_link.disabled {
197 .arrow_comment_link.disabled {
198 cursor: default;
198 cursor: default;
199 color: @grey5;
199 color: @grey5;
200 }
200 }
201 }
201 }
202
202
203 .comment-next-link {
203 .comment-next-link {
204 display: inline-block;
204 display: inline-block;
205
205
206 .arrow_comment_link{
206 .arrow_comment_link{
207 cursor: pointer;
207 cursor: pointer;
208 i {
208 i {
209 font-size:10px;
209 font-size:10px;
210 }
210 }
211 }
211 }
212 .arrow_comment_link.disabled {
212 .arrow_comment_link.disabled {
213 cursor: default;
213 cursor: default;
214 color: @grey5;
214 color: @grey5;
215 }
215 }
216 }
216 }
217
217
218 .flag_status {
218 .flag_status {
219 display: inline-block;
219 display: inline-block;
220 margin: -2px .5em 0 .25em
220 margin: -2px .5em 0 .25em
221 }
221 }
222
222
223 .delete-comment {
223 .delete-comment {
224 display: inline-block;
224 display: inline-block;
225 color: @rcblue;
225 color: @rcblue;
226
226
227 &:hover {
227 &:hover {
228 cursor: pointer;
228 cursor: pointer;
229 }
229 }
230 }
230 }
231
231
232 .text {
232 .text {
233 clear: both;
233 clear: both;
234 .border-radius(@border-radius);
234 .border-radius(@border-radius);
235 .box-sizing(border-box);
235 .box-sizing(border-box);
236
236
237 .markdown-block p,
237 .markdown-block p,
238 .rst-block p {
238 .rst-block p {
239 margin: .5em 0 !important;
239 margin: .5em 0 !important;
240 // TODO: lisa: This is needed because of other rst !important rules :[
240 // TODO: lisa: This is needed because of other rst !important rules :[
241 }
241 }
242 }
242 }
243
243
244 .pr-version {
244 .pr-version {
245 float: left;
245 float: left;
246 margin: 0px 4px;
246 margin: 0px 4px;
247 }
247 }
248 .pr-version-inline {
248 .pr-version-inline {
249 float: left;
249 float: left;
250 margin: 0px 4px;
250 margin: 0px 4px;
251 }
251 }
252 .pr-version-num {
252 .pr-version-num {
253 font-size: 10px;
253 font-size: 10px;
254 }
254 }
255 }
255 }
256
256
257 @comment-padding: 5px;
257 @comment-padding: 5px;
258
258
259 .general-comments {
259 .general-comments {
260 .comment-outdated {
260 .comment-outdated {
261 opacity: @comment-outdated-opacity;
261 opacity: @comment-outdated-opacity;
262 }
262 }
263 }
263 }
264
264
265 .inline-comments {
265 .inline-comments {
266 border-radius: @border-radius;
266 border-radius: @border-radius;
267 .comment {
267 .comment {
268 margin: 0;
268 margin: 0;
269 border-radius: @border-radius;
269 border-radius: @border-radius;
270 }
270 }
271 .comment-outdated {
271 .comment-outdated {
272 opacity: @comment-outdated-opacity;
272 opacity: @comment-outdated-opacity;
273 }
273 }
274
274
275 .comment-inline {
275 .comment-inline {
276 background: white;
276 background: white;
277 padding: @comment-padding @comment-padding;
277 padding: @comment-padding @comment-padding;
278 border: @comment-padding solid @grey6;
278 border: @comment-padding solid @grey6;
279
279
280 .text {
280 .text {
281 border: none;
281 border: none;
282 }
282 }
283 .meta {
283 .meta {
284 border-bottom: 1px solid @grey6;
284 border-bottom: 1px solid @grey6;
285 margin: -5px 0px;
285 margin: -5px 0px;
286 line-height: 24px;
286 line-height: 24px;
287 }
287 }
288 }
288 }
289 .comment-selected {
289 .comment-selected {
290 border-left: 6px solid @comment-highlight-color;
290 border-left: 6px solid @comment-highlight-color;
291 }
291 }
292 .comment-inline-form {
292 .comment-inline-form {
293 padding: @comment-padding;
293 padding: @comment-padding;
294 display: none;
294 display: none;
295 }
295 }
296 .cb-comment-add-button {
296 .cb-comment-add-button {
297 margin: @comment-padding;
297 margin: @comment-padding;
298 }
298 }
299 /* hide add comment button when form is open */
299 /* hide add comment button when form is open */
300 .comment-inline-form-open ~ .cb-comment-add-button {
300 .comment-inline-form-open ~ .cb-comment-add-button {
301 display: none;
301 display: none;
302 }
302 }
303 .comment-inline-form-open {
303 .comment-inline-form-open {
304 display: block;
304 display: block;
305 }
305 }
306 /* hide add comment button when form but no comments */
306 /* hide add comment button when form but no comments */
307 .comment-inline-form:first-child + .cb-comment-add-button {
307 .comment-inline-form:first-child + .cb-comment-add-button {
308 display: none;
308 display: none;
309 }
309 }
310 /* hide add comment button when no comments or form */
310 /* hide add comment button when no comments or form */
311 .cb-comment-add-button:first-child {
311 .cb-comment-add-button:first-child {
312 display: none;
312 display: none;
313 }
313 }
314 /* hide add comment button when only comment is being deleted */
314 /* hide add comment button when only comment is being deleted */
315 .comment-deleting:first-child + .cb-comment-add-button {
315 .comment-deleting:first-child + .cb-comment-add-button {
316 display: none;
316 display: none;
317 }
317 }
318 }
318 }
319
319
320
320
321 .show-outdated-comments {
321 .show-outdated-comments {
322 display: inline;
322 display: inline;
323 color: @rcblue;
323 color: @rcblue;
324 }
324 }
325
325
326 // Comment Form
326 // Comment Form
327 div.comment-form {
327 div.comment-form {
328 margin-top: 20px;
328 margin-top: 20px;
329 }
329 }
330
330
331 .comment-form strong {
331 .comment-form strong {
332 display: block;
332 display: block;
333 margin-bottom: 15px;
333 margin-bottom: 15px;
334 }
334 }
335
335
336 .comment-form textarea {
336 .comment-form textarea {
337 width: 100%;
337 width: 100%;
338 height: 100px;
338 height: 100px;
339 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
339 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
340 }
340 }
341
341
342 form.comment-form {
342 form.comment-form {
343 margin-top: 10px;
343 margin-top: 10px;
344 margin-left: 10px;
344 margin-left: 10px;
345 }
345 }
346
346
347 .comment-inline-form .comment-block-ta,
347 .comment-inline-form .comment-block-ta,
348 .comment-form .comment-block-ta,
348 .comment-form .comment-block-ta,
349 .comment-form .preview-box {
349 .comment-form .preview-box {
350 .border-radius(@border-radius);
350 .border-radius(@border-radius);
351 .box-sizing(border-box);
351 .box-sizing(border-box);
352 background-color: white;
352 background-color: white;
353 }
353 }
354
354
355 .comment-form-submit {
355 .comment-form-submit {
356 margin-top: 5px;
356 margin-top: 5px;
357 margin-left: 525px;
357 margin-left: 525px;
358 }
358 }
359
359
360 .file-comments {
360 .file-comments {
361 display: none;
361 display: none;
362 }
362 }
363
363
364 .comment-form .preview-box.unloaded,
364 .comment-form .preview-box.unloaded,
365 .comment-inline-form .preview-box.unloaded {
365 .comment-inline-form .preview-box.unloaded {
366 height: 50px;
366 height: 50px;
367 text-align: center;
367 text-align: center;
368 padding: 20px;
368 padding: 20px;
369 background-color: white;
369 background-color: white;
370 }
370 }
371
371
372 .comment-footer {
372 .comment-footer {
373 position: relative;
373 position: relative;
374 width: 100%;
374 width: 100%;
375 min-height: 42px;
375 min-height: 42px;
376
376
377 .status_box,
377 .status_box,
378 .cancel-button {
378 .cancel-button {
379 float: left;
379 float: left;
380 display: inline-block;
380 display: inline-block;
381 }
381 }
382
382
383 .action-buttons {
383 .action-buttons {
384 float: right;
384 float: right;
385 display: inline-block;
385 display: inline-block;
386 }
386 }
387
388 .action-buttons-extra {
389 display: inline-block;
390 }
387 }
391 }
388
392
389 .comment-form {
393 .comment-form {
390
394
391 .comment {
395 .comment {
392 margin-left: 10px;
396 margin-left: 10px;
393 }
397 }
394
398
395 .comment-help {
399 .comment-help {
396 color: @grey4;
400 color: @grey4;
397 padding: 5px 0 5px 0;
401 padding: 5px 0 5px 0;
398 }
402 }
399
403
400 .comment-title {
404 .comment-title {
401 padding: 5px 0 5px 0;
405 padding: 5px 0 5px 0;
402 }
406 }
403
407
404 .comment-button {
408 .comment-button {
405 display: inline-block;
409 display: inline-block;
406 }
410 }
407
411
408 .comment-button-input {
412 .comment-button-input {
409 margin-right: 0;
413 margin-right: 0;
410 }
414 }
411
415
412 .comment-footer {
416 .comment-footer {
413 margin-bottom: 110px;
417 margin-bottom: 110px;
414 margin-top: 10px;
418 margin-top: 10px;
415 }
419 }
416 }
420 }
417
421
418
422
419 .comment-form-login {
423 .comment-form-login {
420 .comment-help {
424 .comment-help {
421 padding: 0.9em; //same as the button
425 padding: 0.9em; //same as the button
422 }
426 }
423
427
424 div.clearfix {
428 div.clearfix {
425 clear: both;
429 clear: both;
426 width: 100%;
430 width: 100%;
427 display: block;
431 display: block;
428 }
432 }
429 }
433 }
430
434
431 .comment-type {
435 .comment-type {
432 margin: 0px;
436 margin: 0px;
433 border-radius: inherit;
437 border-radius: inherit;
434 border-color: @grey6;
438 border-color: @grey6;
435 }
439 }
436
440
437 .preview-box {
441 .preview-box {
438 min-height: 105px;
442 min-height: 105px;
439 margin-bottom: 15px;
443 margin-bottom: 15px;
440 background-color: white;
444 background-color: white;
441 .border-radius(@border-radius);
445 .border-radius(@border-radius);
442 .box-sizing(border-box);
446 .box-sizing(border-box);
443 }
447 }
444
448
445 .add-another-button {
449 .add-another-button {
446 margin-left: 10px;
450 margin-left: 10px;
447 margin-top: 10px;
451 margin-top: 10px;
448 margin-bottom: 10px;
452 margin-bottom: 10px;
449 }
453 }
450
454
451 .comment .buttons {
455 .comment .buttons {
452 float: right;
456 float: right;
453 margin: -1px 0px 0px 0px;
457 margin: -1px 0px 0px 0px;
454 }
458 }
455
459
456 // Inline Comment Form
460 // Inline Comment Form
457 .injected_diff .comment-inline-form,
461 .injected_diff .comment-inline-form,
458 .comment-inline-form {
462 .comment-inline-form {
459 background-color: white;
463 background-color: white;
460 margin-top: 10px;
464 margin-top: 10px;
461 margin-bottom: 20px;
465 margin-bottom: 20px;
462 }
466 }
463
467
464 .inline-form {
468 .inline-form {
465 padding: 10px 7px;
469 padding: 10px 7px;
466 }
470 }
467
471
468 .inline-form div {
472 .inline-form div {
469 max-width: 100%;
473 max-width: 100%;
470 }
474 }
471
475
472 .overlay {
476 .overlay {
473 display: none;
477 display: none;
474 position: absolute;
478 position: absolute;
475 width: 100%;
479 width: 100%;
476 text-align: center;
480 text-align: center;
477 vertical-align: middle;
481 vertical-align: middle;
478 font-size: 16px;
482 font-size: 16px;
479 background: none repeat scroll 0 0 white;
483 background: none repeat scroll 0 0 white;
480
484
481 &.submitting {
485 &.submitting {
482 display: block;
486 display: block;
483 opacity: 0.5;
487 opacity: 0.5;
484 z-index: 100;
488 z-index: 100;
485 }
489 }
486 }
490 }
487 .comment-inline-form .overlay.submitting .overlay-text {
491 .comment-inline-form .overlay.submitting .overlay-text {
488 margin-top: 5%;
492 margin-top: 5%;
489 }
493 }
490
494
491 .comment-inline-form .clearfix,
495 .comment-inline-form .clearfix,
492 .comment-form .clearfix {
496 .comment-form .clearfix {
493 .border-radius(@border-radius);
497 .border-radius(@border-radius);
494 margin: 0px;
498 margin: 0px;
495 }
499 }
496
500
497 .comment-inline-form .comment-footer {
501 .comment-inline-form .comment-footer {
498 margin: 10px 0px 0px 0px;
502 margin: 10px 0px 0px 0px;
499 }
503 }
500
504
501 .hide-inline-form-button {
505 .hide-inline-form-button {
502 margin-left: 5px;
506 margin-left: 5px;
503 }
507 }
504 .comment-button .hide-inline-form {
508 .comment-button .hide-inline-form {
505 background: white;
509 background: white;
506 }
510 }
507
511
508 .comment-area {
512 .comment-area {
509 padding: 8px 12px;
513 padding: 8px 12px;
510 border: 1px solid @grey5;
514 border: 1px solid @grey5;
511 .border-radius(@border-radius);
515 .border-radius(@border-radius);
512
516
513 .resolve-action {
517 .resolve-action {
514 padding: 1px 0px 0px 6px;
518 padding: 1px 0px 0px 6px;
515 }
519 }
516
520
517 }
521 }
518
522
519 .comment-area-header .nav-links {
523 .comment-area-header .nav-links {
520 display: flex;
524 display: flex;
521 flex-flow: row wrap;
525 flex-flow: row wrap;
522 -webkit-flex-flow: row wrap;
526 -webkit-flex-flow: row wrap;
523 width: 100%;
527 width: 100%;
524 }
528 }
525
529
526 .comment-area-footer {
530 .comment-area-footer {
527 display: flex;
531 display: flex;
528 }
532 }
529
533
530 .comment-footer .toolbar {
534 .comment-footer .toolbar {
531
535
532 }
536 }
533
537
534 .nav-links {
538 .nav-links {
535 padding: 0;
539 padding: 0;
536 margin: 0;
540 margin: 0;
537 list-style: none;
541 list-style: none;
538 height: auto;
542 height: auto;
539 border-bottom: 1px solid @grey5;
543 border-bottom: 1px solid @grey5;
540 }
544 }
541 .nav-links li {
545 .nav-links li {
542 display: inline-block;
546 display: inline-block;
543 }
547 }
544 .nav-links li:before {
548 .nav-links li:before {
545 content: "";
549 content: "";
546 }
550 }
547 .nav-links li a.disabled {
551 .nav-links li a.disabled {
548 cursor: not-allowed;
552 cursor: not-allowed;
549 }
553 }
550
554
551 .nav-links li.active a {
555 .nav-links li.active a {
552 border-bottom: 2px solid @rcblue;
556 border-bottom: 2px solid @rcblue;
553 color: #000;
557 color: #000;
554 font-weight: 600;
558 font-weight: 600;
555 }
559 }
556 .nav-links li a {
560 .nav-links li a {
557 display: inline-block;
561 display: inline-block;
558 padding: 0px 10px 5px 10px;
562 padding: 0px 10px 5px 10px;
559 margin-bottom: -1px;
563 margin-bottom: -1px;
560 font-size: 14px;
564 font-size: 14px;
561 line-height: 28px;
565 line-height: 28px;
562 color: #8f8f8f;
566 color: #8f8f8f;
563 border-bottom: 2px solid transparent;
567 border-bottom: 2px solid transparent;
564 }
568 }
565
569
566 .toolbar-text {
570 .toolbar-text {
567 float: left;
571 float: left;
568 margin: -5px 0px 0px 0px;
572 margin: -5px 0px 0px 0px;
569 font-size: 12px;
573 font-size: 12px;
570 }
574 }
571
575
@@ -1,813 +1,830 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45 /* Comment form for main and inline comments */
45 /* Comment form for main and inline comments */
46 (function(mod) {
46 (function(mod) {
47
47
48 if (typeof exports == "object" && typeof module == "object") {
48 if (typeof exports == "object" && typeof module == "object") {
49 // CommonJS
49 // CommonJS
50 module.exports = mod();
50 module.exports = mod();
51 }
51 }
52 else {
52 else {
53 // Plain browser env
53 // Plain browser env
54 (this || window).CommentForm = mod();
54 (this || window).CommentForm = mod();
55 }
55 }
56
56
57 })(function() {
57 })(function() {
58 "use strict";
58 "use strict";
59
59
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
61 if (!(this instanceof CommentForm)) {
61 if (!(this instanceof CommentForm)) {
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
63 }
63 }
64
64
65 // bind the element instance to our Form
65 // bind the element instance to our Form
66 $(formElement).get(0).CommentForm = this;
66 $(formElement).get(0).CommentForm = this;
67
67
68 this.withLineNo = function(selector) {
68 this.withLineNo = function(selector) {
69 var lineNo = this.lineNo;
69 var lineNo = this.lineNo;
70 if (lineNo === undefined) {
70 if (lineNo === undefined) {
71 return selector
71 return selector
72 } else {
72 } else {
73 return selector + '_' + lineNo;
73 return selector + '_' + lineNo;
74 }
74 }
75 };
75 };
76
76
77 this.commitId = commitId;
77 this.commitId = commitId;
78 this.pullRequestId = pullRequestId;
78 this.pullRequestId = pullRequestId;
79 this.lineNo = lineNo;
79 this.lineNo = lineNo;
80 this.initAutocompleteActions = initAutocompleteActions;
80 this.initAutocompleteActions = initAutocompleteActions;
81
81
82 this.previewButton = this.withLineNo('#preview-btn');
82 this.previewButton = this.withLineNo('#preview-btn');
83 this.previewContainer = this.withLineNo('#preview-container');
83 this.previewContainer = this.withLineNo('#preview-container');
84
84
85 this.previewBoxSelector = this.withLineNo('#preview-box');
85 this.previewBoxSelector = this.withLineNo('#preview-box');
86
86
87 this.editButton = this.withLineNo('#edit-btn');
87 this.editButton = this.withLineNo('#edit-btn');
88 this.editContainer = this.withLineNo('#edit-container');
88 this.editContainer = this.withLineNo('#edit-container');
89 this.cancelButton = this.withLineNo('#cancel-btn');
89 this.cancelButton = this.withLineNo('#cancel-btn');
90 this.commentType = this.withLineNo('#comment_type');
90 this.commentType = this.withLineNo('#comment_type');
91
91
92 this.resolvesId = null;
92 this.resolvesId = null;
93 this.resolvesActionId = null;
93 this.resolvesActionId = null;
94
94
95 this.closesPr = '#close_pull_request';
96
95 this.cmBox = this.withLineNo('#text');
97 this.cmBox = this.withLineNo('#text');
96 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
98 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
97
99
98 this.statusChange = this.withLineNo('#change_status');
100 this.statusChange = this.withLineNo('#change_status');
99
101
100 this.submitForm = formElement;
102 this.submitForm = formElement;
101 this.submitButton = $(this.submitForm).find('input[type="submit"]');
103 this.submitButton = $(this.submitForm).find('input[type="submit"]');
102 this.submitButtonText = this.submitButton.val();
104 this.submitButtonText = this.submitButton.val();
103
105
104 this.previewUrl = pyroutes.url('changeset_comment_preview',
106 this.previewUrl = pyroutes.url('changeset_comment_preview',
105 {'repo_name': templateContext.repo_name});
107 {'repo_name': templateContext.repo_name});
106
108
107 if (resolvesCommentId){
109 if (resolvesCommentId){
108 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
110 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
109 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
111 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
110 $(this.commentType).prop('disabled', true);
112 $(this.commentType).prop('disabled', true);
111 $(this.commentType).addClass('disabled');
113 $(this.commentType).addClass('disabled');
112
114
113 // disable select
115 // disable select
114 setTimeout(function() {
116 setTimeout(function() {
115 $(self.statusChange).select2('readonly', true);
117 $(self.statusChange).select2('readonly', true);
116 }, 10);
118 }, 10);
117
119
118 var resolvedInfo = (
120 var resolvedInfo = (
119 '<li class="resolve-action">' +
121 '<li class="resolve-action">' +
120 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
122 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
121 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
123 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
122 '</li>'
124 '</li>'
123 ).format(resolvesCommentId, _gettext('resolve comment'));
125 ).format(resolvesCommentId, _gettext('resolve comment'));
124 $(resolvedInfo).insertAfter($(this.commentType).parent());
126 $(resolvedInfo).insertAfter($(this.commentType).parent());
125 }
127 }
126
128
127 // based on commitId, or pullRequestId decide where do we submit
129 // based on commitId, or pullRequestId decide where do we submit
128 // out data
130 // out data
129 if (this.commitId){
131 if (this.commitId){
130 this.submitUrl = pyroutes.url('changeset_comment',
132 this.submitUrl = pyroutes.url('changeset_comment',
131 {'repo_name': templateContext.repo_name,
133 {'repo_name': templateContext.repo_name,
132 'revision': this.commitId});
134 'revision': this.commitId});
133 this.selfUrl = pyroutes.url('changeset_home',
135 this.selfUrl = pyroutes.url('changeset_home',
134 {'repo_name': templateContext.repo_name,
136 {'repo_name': templateContext.repo_name,
135 'revision': this.commitId});
137 'revision': this.commitId});
136
138
137 } else if (this.pullRequestId) {
139 } else if (this.pullRequestId) {
138 this.submitUrl = pyroutes.url('pullrequest_comment',
140 this.submitUrl = pyroutes.url('pullrequest_comment',
139 {'repo_name': templateContext.repo_name,
141 {'repo_name': templateContext.repo_name,
140 'pull_request_id': this.pullRequestId});
142 'pull_request_id': this.pullRequestId});
141 this.selfUrl = pyroutes.url('pullrequest_show',
143 this.selfUrl = pyroutes.url('pullrequest_show',
142 {'repo_name': templateContext.repo_name,
144 {'repo_name': templateContext.repo_name,
143 'pull_request_id': this.pullRequestId});
145 'pull_request_id': this.pullRequestId});
144
146
145 } else {
147 } else {
146 throw new Error(
148 throw new Error(
147 'CommentForm requires pullRequestId, or commitId to be specified.')
149 'CommentForm requires pullRequestId, or commitId to be specified.')
148 }
150 }
149
151
150 // FUNCTIONS and helpers
152 // FUNCTIONS and helpers
151 var self = this;
153 var self = this;
152
154
153 this.isInline = function(){
155 this.isInline = function(){
154 return this.lineNo && this.lineNo != 'general';
156 return this.lineNo && this.lineNo != 'general';
155 };
157 };
156
158
157 this.getCmInstance = function(){
159 this.getCmInstance = function(){
158 return this.cm
160 return this.cm
159 };
161 };
160
162
161 this.setPlaceholder = function(placeholder) {
163 this.setPlaceholder = function(placeholder) {
162 var cm = this.getCmInstance();
164 var cm = this.getCmInstance();
163 if (cm){
165 if (cm){
164 cm.setOption('placeholder', placeholder);
166 cm.setOption('placeholder', placeholder);
165 }
167 }
166 };
168 };
167
169
168 this.getCommentStatus = function() {
170 this.getCommentStatus = function() {
169 return $(this.submitForm).find(this.statusChange).val();
171 return $(this.submitForm).find(this.statusChange).val();
170 };
172 };
171 this.getCommentType = function() {
173 this.getCommentType = function() {
172 return $(this.submitForm).find(this.commentType).val();
174 return $(this.submitForm).find(this.commentType).val();
173 };
175 };
174
176
175 this.getResolvesId = function() {
177 this.getResolvesId = function() {
176 return $(this.submitForm).find(this.resolvesId).val() || null;
178 return $(this.submitForm).find(this.resolvesId).val() || null;
177 };
179 };
180
181 this.getClosePr = function() {
182 return $(this.submitForm).find(this.closesPr).val() || null;
183 };
184
178 this.markCommentResolved = function(resolvedCommentId){
185 this.markCommentResolved = function(resolvedCommentId){
179 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
186 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
180 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
187 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
181 };
188 };
182
189
183 this.isAllowedToSubmit = function() {
190 this.isAllowedToSubmit = function() {
184 return !$(this.submitButton).prop('disabled');
191 return !$(this.submitButton).prop('disabled');
185 };
192 };
186
193
187 this.initStatusChangeSelector = function(){
194 this.initStatusChangeSelector = function(){
188 var formatChangeStatus = function(state, escapeMarkup) {
195 var formatChangeStatus = function(state, escapeMarkup) {
189 var originalOption = state.element;
196 var originalOption = state.element;
190 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
197 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
191 '<span>' + escapeMarkup(state.text) + '</span>';
198 '<span>' + escapeMarkup(state.text) + '</span>';
192 };
199 };
193 var formatResult = function(result, container, query, escapeMarkup) {
200 var formatResult = function(result, container, query, escapeMarkup) {
194 return formatChangeStatus(result, escapeMarkup);
201 return formatChangeStatus(result, escapeMarkup);
195 };
202 };
196
203
197 var formatSelection = function(data, container, escapeMarkup) {
204 var formatSelection = function(data, container, escapeMarkup) {
198 return formatChangeStatus(data, escapeMarkup);
205 return formatChangeStatus(data, escapeMarkup);
199 };
206 };
200
207
201 $(this.submitForm).find(this.statusChange).select2({
208 $(this.submitForm).find(this.statusChange).select2({
202 placeholder: _gettext('Status Review'),
209 placeholder: _gettext('Status Review'),
203 formatResult: formatResult,
210 formatResult: formatResult,
204 formatSelection: formatSelection,
211 formatSelection: formatSelection,
205 containerCssClass: "drop-menu status_box_menu",
212 containerCssClass: "drop-menu status_box_menu",
206 dropdownCssClass: "drop-menu-dropdown",
213 dropdownCssClass: "drop-menu-dropdown",
207 dropdownAutoWidth: true,
214 dropdownAutoWidth: true,
208 minimumResultsForSearch: -1
215 minimumResultsForSearch: -1
209 });
216 });
210 $(this.submitForm).find(this.statusChange).on('change', function() {
217 $(this.submitForm).find(this.statusChange).on('change', function() {
211 var status = self.getCommentStatus();
218 var status = self.getCommentStatus();
219
212 if (status && !self.isInline()) {
220 if (status && !self.isInline()) {
213 $(self.submitButton).prop('disabled', false);
221 $(self.submitButton).prop('disabled', false);
214 }
222 }
215
223
216 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
224 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
217 self.setPlaceholder(placeholderText)
225 self.setPlaceholder(placeholderText)
218 })
226 })
219 };
227 };
220
228
221 // reset the comment form into it's original state
229 // reset the comment form into it's original state
222 this.resetCommentFormState = function(content) {
230 this.resetCommentFormState = function(content) {
223 content = content || '';
231 content = content || '';
224
232
225 $(this.editContainer).show();
233 $(this.editContainer).show();
226 $(this.editButton).parent().addClass('active');
234 $(this.editButton).parent().addClass('active');
227
235
228 $(this.previewContainer).hide();
236 $(this.previewContainer).hide();
229 $(this.previewButton).parent().removeClass('active');
237 $(this.previewButton).parent().removeClass('active');
230
238
231 this.setActionButtonsDisabled(true);
239 this.setActionButtonsDisabled(true);
232 self.cm.setValue(content);
240 self.cm.setValue(content);
233 self.cm.setOption("readOnly", false);
241 self.cm.setOption("readOnly", false);
234
242
235 if (this.resolvesId) {
243 if (this.resolvesId) {
236 // destroy the resolve action
244 // destroy the resolve action
237 $(this.resolvesId).parent().remove();
245 $(this.resolvesId).parent().remove();
238 }
246 }
247 // reset closingPR flag
248 $('.close-pr-input').remove();
239
249
240 $(this.statusChange).select2('readonly', false);
250 $(this.statusChange).select2('readonly', false);
241 };
251 };
242
252
243 this.globalSubmitSuccessCallback = function(){
253 this.globalSubmitSuccessCallback = function(){
244 // default behaviour is to call GLOBAL hook, if it's registered.
254 // default behaviour is to call GLOBAL hook, if it's registered.
245 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
255 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
246 commentFormGlobalSubmitSuccessCallback()
256 commentFormGlobalSubmitSuccessCallback()
247 }
257 }
248 };
258 };
249
259
250 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
260 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
251 failHandler = failHandler || function() {};
261 failHandler = failHandler || function() {};
252 var postData = toQueryString(postData);
262 var postData = toQueryString(postData);
253 var request = $.ajax({
263 var request = $.ajax({
254 url: url,
264 url: url,
255 type: 'POST',
265 type: 'POST',
256 data: postData,
266 data: postData,
257 headers: {'X-PARTIAL-XHR': true}
267 headers: {'X-PARTIAL-XHR': true}
258 })
268 })
259 .done(function(data) {
269 .done(function(data) {
260 successHandler(data);
270 successHandler(data);
261 })
271 })
262 .fail(function(data, textStatus, errorThrown){
272 .fail(function(data, textStatus, errorThrown){
263 alert(
273 alert(
264 "Error while submitting comment.\n" +
274 "Error while submitting comment.\n" +
265 "Error code {0} ({1}).".format(data.status, data.statusText));
275 "Error code {0} ({1}).".format(data.status, data.statusText));
266 failHandler()
276 failHandler()
267 });
277 });
268 return request;
278 return request;
269 };
279 };
270
280
271 // overwrite a submitHandler, we need to do it for inline comments
281 // overwrite a submitHandler, we need to do it for inline comments
272 this.setHandleFormSubmit = function(callback) {
282 this.setHandleFormSubmit = function(callback) {
273 this.handleFormSubmit = callback;
283 this.handleFormSubmit = callback;
274 };
284 };
275
285
276 // overwrite a submitSuccessHandler
286 // overwrite a submitSuccessHandler
277 this.setGlobalSubmitSuccessCallback = function(callback) {
287 this.setGlobalSubmitSuccessCallback = function(callback) {
278 this.globalSubmitSuccessCallback = callback;
288 this.globalSubmitSuccessCallback = callback;
279 };
289 };
280
290
281 // default handler for for submit for main comments
291 // default handler for for submit for main comments
282 this.handleFormSubmit = function() {
292 this.handleFormSubmit = function() {
283 var text = self.cm.getValue();
293 var text = self.cm.getValue();
284 var status = self.getCommentStatus();
294 var status = self.getCommentStatus();
285 var commentType = self.getCommentType();
295 var commentType = self.getCommentType();
286 var resolvesCommentId = self.getResolvesId();
296 var resolvesCommentId = self.getResolvesId();
297 var closePullRequest = self.getClosePr();
287
298
288 if (text === "" && !status) {
299 if (text === "" && !status) {
289 return;
300 return;
290 }
301 }
291
302
292 var excludeCancelBtn = false;
303 var excludeCancelBtn = false;
293 var submitEvent = true;
304 var submitEvent = true;
294 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
305 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
295 self.cm.setOption("readOnly", true);
306 self.cm.setOption("readOnly", true);
296
307
297 var postData = {
308 var postData = {
298 'text': text,
309 'text': text,
299 'changeset_status': status,
310 'changeset_status': status,
300 'comment_type': commentType,
311 'comment_type': commentType,
301 'csrf_token': CSRF_TOKEN
312 'csrf_token': CSRF_TOKEN
302 };
313 };
303 if (resolvesCommentId){
314
315 if (resolvesCommentId) {
304 postData['resolves_comment_id'] = resolvesCommentId;
316 postData['resolves_comment_id'] = resolvesCommentId;
305 }
317 }
306
318
319 if (closePullRequest) {
320 postData['close_pull_request'] = true;
321 }
322
307 var submitSuccessCallback = function(o) {
323 var submitSuccessCallback = function(o) {
308 // reload page if we change status for single commit.
324 // reload page if we change status for single commit.
309 if (status && self.commitId) {
325 if (status && self.commitId) {
310 location.reload(true);
326 location.reload(true);
311 } else {
327 } else {
312 $('#injected_page_comments').append(o.rendered_text);
328 $('#injected_page_comments').append(o.rendered_text);
313 self.resetCommentFormState();
329 self.resetCommentFormState();
314 timeagoActivate();
330 timeagoActivate();
315
331
316 // mark visually which comment was resolved
332 // mark visually which comment was resolved
317 if (resolvesCommentId) {
333 if (resolvesCommentId) {
318 self.markCommentResolved(resolvesCommentId);
334 self.markCommentResolved(resolvesCommentId);
319 }
335 }
320 }
336 }
321
337
322 // run global callback on submit
338 // run global callback on submit
323 self.globalSubmitSuccessCallback();
339 self.globalSubmitSuccessCallback();
324
340
325 };
341 };
326 var submitFailCallback = function(){
342 var submitFailCallback = function(){
327 self.resetCommentFormState(text);
343 self.resetCommentFormState(text);
328 };
344 };
329 self.submitAjaxPOST(
345 self.submitAjaxPOST(
330 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
346 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
331 };
347 };
332
348
333 this.previewSuccessCallback = function(o) {
349 this.previewSuccessCallback = function(o) {
334 $(self.previewBoxSelector).html(o);
350 $(self.previewBoxSelector).html(o);
335 $(self.previewBoxSelector).removeClass('unloaded');
351 $(self.previewBoxSelector).removeClass('unloaded');
336
352
337 // swap buttons, making preview active
353 // swap buttons, making preview active
338 $(self.previewButton).parent().addClass('active');
354 $(self.previewButton).parent().addClass('active');
339 $(self.editButton).parent().removeClass('active');
355 $(self.editButton).parent().removeClass('active');
340
356
341 // unlock buttons
357 // unlock buttons
342 self.setActionButtonsDisabled(false);
358 self.setActionButtonsDisabled(false);
343 };
359 };
344
360
345 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
361 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
346 excludeCancelBtn = excludeCancelBtn || false;
362 excludeCancelBtn = excludeCancelBtn || false;
347 submitEvent = submitEvent || false;
363 submitEvent = submitEvent || false;
348
364
349 $(this.editButton).prop('disabled', state);
365 $(this.editButton).prop('disabled', state);
350 $(this.previewButton).prop('disabled', state);
366 $(this.previewButton).prop('disabled', state);
351
367
352 if (!excludeCancelBtn) {
368 if (!excludeCancelBtn) {
353 $(this.cancelButton).prop('disabled', state);
369 $(this.cancelButton).prop('disabled', state);
354 }
370 }
355
371
356 var submitState = state;
372 var submitState = state;
357 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
373 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
358 // if the value of commit review status is set, we allow
374 // if the value of commit review status is set, we allow
359 // submit button, but only on Main form, isInline means inline
375 // submit button, but only on Main form, isInline means inline
360 submitState = false
376 submitState = false
361 }
377 }
378
362 $(this.submitButton).prop('disabled', submitState);
379 $(this.submitButton).prop('disabled', submitState);
363 if (submitEvent) {
380 if (submitEvent) {
364 $(this.submitButton).val(_gettext('Submitting...'));
381 $(this.submitButton).val(_gettext('Submitting...'));
365 } else {
382 } else {
366 $(this.submitButton).val(this.submitButtonText);
383 $(this.submitButton).val(this.submitButtonText);
367 }
384 }
368
385
369 };
386 };
370
387
371 // lock preview/edit/submit buttons on load, but exclude cancel button
388 // lock preview/edit/submit buttons on load, but exclude cancel button
372 var excludeCancelBtn = true;
389 var excludeCancelBtn = true;
373 this.setActionButtonsDisabled(true, excludeCancelBtn);
390 this.setActionButtonsDisabled(true, excludeCancelBtn);
374
391
375 // anonymous users don't have access to initialized CM instance
392 // anonymous users don't have access to initialized CM instance
376 if (this.cm !== undefined){
393 if (this.cm !== undefined){
377 this.cm.on('change', function(cMirror) {
394 this.cm.on('change', function(cMirror) {
378 if (cMirror.getValue() === "") {
395 if (cMirror.getValue() === "") {
379 self.setActionButtonsDisabled(true, excludeCancelBtn)
396 self.setActionButtonsDisabled(true, excludeCancelBtn)
380 } else {
397 } else {
381 self.setActionButtonsDisabled(false, excludeCancelBtn)
398 self.setActionButtonsDisabled(false, excludeCancelBtn)
382 }
399 }
383 });
400 });
384 }
401 }
385
402
386 $(this.editButton).on('click', function(e) {
403 $(this.editButton).on('click', function(e) {
387 e.preventDefault();
404 e.preventDefault();
388
405
389 $(self.previewButton).parent().removeClass('active');
406 $(self.previewButton).parent().removeClass('active');
390 $(self.previewContainer).hide();
407 $(self.previewContainer).hide();
391
408
392 $(self.editButton).parent().addClass('active');
409 $(self.editButton).parent().addClass('active');
393 $(self.editContainer).show();
410 $(self.editContainer).show();
394
411
395 });
412 });
396
413
397 $(this.previewButton).on('click', function(e) {
414 $(this.previewButton).on('click', function(e) {
398 e.preventDefault();
415 e.preventDefault();
399 var text = self.cm.getValue();
416 var text = self.cm.getValue();
400
417
401 if (text === "") {
418 if (text === "") {
402 return;
419 return;
403 }
420 }
404
421
405 var postData = {
422 var postData = {
406 'text': text,
423 'text': text,
407 'renderer': templateContext.visual.default_renderer,
424 'renderer': templateContext.visual.default_renderer,
408 'csrf_token': CSRF_TOKEN
425 'csrf_token': CSRF_TOKEN
409 };
426 };
410
427
411 // lock ALL buttons on preview
428 // lock ALL buttons on preview
412 self.setActionButtonsDisabled(true);
429 self.setActionButtonsDisabled(true);
413
430
414 $(self.previewBoxSelector).addClass('unloaded');
431 $(self.previewBoxSelector).addClass('unloaded');
415 $(self.previewBoxSelector).html(_gettext('Loading ...'));
432 $(self.previewBoxSelector).html(_gettext('Loading ...'));
416
433
417 $(self.editContainer).hide();
434 $(self.editContainer).hide();
418 $(self.previewContainer).show();
435 $(self.previewContainer).show();
419
436
420 // by default we reset state of comment preserving the text
437 // by default we reset state of comment preserving the text
421 var previewFailCallback = function(){
438 var previewFailCallback = function(){
422 self.resetCommentFormState(text)
439 self.resetCommentFormState(text)
423 };
440 };
424 self.submitAjaxPOST(
441 self.submitAjaxPOST(
425 self.previewUrl, postData, self.previewSuccessCallback,
442 self.previewUrl, postData, self.previewSuccessCallback,
426 previewFailCallback);
443 previewFailCallback);
427
444
428 $(self.previewButton).parent().addClass('active');
445 $(self.previewButton).parent().addClass('active');
429 $(self.editButton).parent().removeClass('active');
446 $(self.editButton).parent().removeClass('active');
430 });
447 });
431
448
432 $(this.submitForm).submit(function(e) {
449 $(this.submitForm).submit(function(e) {
433 e.preventDefault();
450 e.preventDefault();
434 var allowedToSubmit = self.isAllowedToSubmit();
451 var allowedToSubmit = self.isAllowedToSubmit();
435 if (!allowedToSubmit){
452 if (!allowedToSubmit){
436 return false;
453 return false;
437 }
454 }
438 self.handleFormSubmit();
455 self.handleFormSubmit();
439 });
456 });
440
457
441 }
458 }
442
459
443 return CommentForm;
460 return CommentForm;
444 });
461 });
445
462
446 /* comments controller */
463 /* comments controller */
447 var CommentsController = function() {
464 var CommentsController = function() {
448 var mainComment = '#text';
465 var mainComment = '#text';
449 var self = this;
466 var self = this;
450
467
451 this.cancelComment = function(node) {
468 this.cancelComment = function(node) {
452 var $node = $(node);
469 var $node = $(node);
453 var $td = $node.closest('td');
470 var $td = $node.closest('td');
454 $node.closest('.comment-inline-form').remove();
471 $node.closest('.comment-inline-form').remove();
455 return false;
472 return false;
456 };
473 };
457
474
458 this.getLineNumber = function(node) {
475 this.getLineNumber = function(node) {
459 var $node = $(node);
476 var $node = $(node);
460 return $node.closest('td').attr('data-line-number');
477 return $node.closest('td').attr('data-line-number');
461 };
478 };
462
479
463 this.scrollToComment = function(node, offset, outdated) {
480 this.scrollToComment = function(node, offset, outdated) {
464 if (offset === undefined) {
481 if (offset === undefined) {
465 offset = 0;
482 offset = 0;
466 }
483 }
467 var outdated = outdated || false;
484 var outdated = outdated || false;
468 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
485 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
469
486
470 if (!node) {
487 if (!node) {
471 node = $('.comment-selected');
488 node = $('.comment-selected');
472 if (!node.length) {
489 if (!node.length) {
473 node = $('comment-current')
490 node = $('comment-current')
474 }
491 }
475 }
492 }
476 $wrapper = $(node).closest('div.comment');
493 $wrapper = $(node).closest('div.comment');
477 $comment = $(node).closest(klass);
494 $comment = $(node).closest(klass);
478 $comments = $(klass);
495 $comments = $(klass);
479
496
480 // show hidden comment when referenced.
497 // show hidden comment when referenced.
481 if (!$wrapper.is(':visible')){
498 if (!$wrapper.is(':visible')){
482 $wrapper.show();
499 $wrapper.show();
483 }
500 }
484
501
485 $('.comment-selected').removeClass('comment-selected');
502 $('.comment-selected').removeClass('comment-selected');
486
503
487 var nextIdx = $(klass).index($comment) + offset;
504 var nextIdx = $(klass).index($comment) + offset;
488 if (nextIdx >= $comments.length) {
505 if (nextIdx >= $comments.length) {
489 nextIdx = 0;
506 nextIdx = 0;
490 }
507 }
491 var $next = $(klass).eq(nextIdx);
508 var $next = $(klass).eq(nextIdx);
492
509
493 var $cb = $next.closest('.cb');
510 var $cb = $next.closest('.cb');
494 $cb.removeClass('cb-collapsed');
511 $cb.removeClass('cb-collapsed');
495
512
496 var $filediffCollapseState = $cb.closest('.filediff').prev();
513 var $filediffCollapseState = $cb.closest('.filediff').prev();
497 $filediffCollapseState.prop('checked', false);
514 $filediffCollapseState.prop('checked', false);
498 $next.addClass('comment-selected');
515 $next.addClass('comment-selected');
499 scrollToElement($next);
516 scrollToElement($next);
500 return false;
517 return false;
501 };
518 };
502
519
503 this.nextComment = function(node) {
520 this.nextComment = function(node) {
504 return self.scrollToComment(node, 1);
521 return self.scrollToComment(node, 1);
505 };
522 };
506
523
507 this.prevComment = function(node) {
524 this.prevComment = function(node) {
508 return self.scrollToComment(node, -1);
525 return self.scrollToComment(node, -1);
509 };
526 };
510
527
511 this.nextOutdatedComment = function(node) {
528 this.nextOutdatedComment = function(node) {
512 return self.scrollToComment(node, 1, true);
529 return self.scrollToComment(node, 1, true);
513 };
530 };
514
531
515 this.prevOutdatedComment = function(node) {
532 this.prevOutdatedComment = function(node) {
516 return self.scrollToComment(node, -1, true);
533 return self.scrollToComment(node, -1, true);
517 };
534 };
518
535
519 this.deleteComment = function(node) {
536 this.deleteComment = function(node) {
520 if (!confirm(_gettext('Delete this comment?'))) {
537 if (!confirm(_gettext('Delete this comment?'))) {
521 return false;
538 return false;
522 }
539 }
523 var $node = $(node);
540 var $node = $(node);
524 var $td = $node.closest('td');
541 var $td = $node.closest('td');
525 var $comment = $node.closest('.comment');
542 var $comment = $node.closest('.comment');
526 var comment_id = $comment.attr('data-comment-id');
543 var comment_id = $comment.attr('data-comment-id');
527 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
544 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
528 var postData = {
545 var postData = {
529 '_method': 'delete',
546 '_method': 'delete',
530 'csrf_token': CSRF_TOKEN
547 'csrf_token': CSRF_TOKEN
531 };
548 };
532
549
533 $comment.addClass('comment-deleting');
550 $comment.addClass('comment-deleting');
534 $comment.hide('fast');
551 $comment.hide('fast');
535
552
536 var success = function(response) {
553 var success = function(response) {
537 $comment.remove();
554 $comment.remove();
538 return false;
555 return false;
539 };
556 };
540 var failure = function(data, textStatus, xhr) {
557 var failure = function(data, textStatus, xhr) {
541 alert("error processing request: " + textStatus);
558 alert("error processing request: " + textStatus);
542 $comment.show('fast');
559 $comment.show('fast');
543 $comment.removeClass('comment-deleting');
560 $comment.removeClass('comment-deleting');
544 return false;
561 return false;
545 };
562 };
546 ajaxPOST(url, postData, success, failure);
563 ajaxPOST(url, postData, success, failure);
547 };
564 };
548
565
549 this.toggleWideMode = function (node) {
566 this.toggleWideMode = function (node) {
550 if ($('#content').hasClass('wrapper')) {
567 if ($('#content').hasClass('wrapper')) {
551 $('#content').removeClass("wrapper");
568 $('#content').removeClass("wrapper");
552 $('#content').addClass("wide-mode-wrapper");
569 $('#content').addClass("wide-mode-wrapper");
553 $(node).addClass('btn-success');
570 $(node).addClass('btn-success');
554 } else {
571 } else {
555 $('#content').removeClass("wide-mode-wrapper");
572 $('#content').removeClass("wide-mode-wrapper");
556 $('#content').addClass("wrapper");
573 $('#content').addClass("wrapper");
557 $(node).removeClass('btn-success');
574 $(node).removeClass('btn-success');
558 }
575 }
559 return false;
576 return false;
560 };
577 };
561
578
562 this.toggleComments = function(node, show) {
579 this.toggleComments = function(node, show) {
563 var $filediff = $(node).closest('.filediff');
580 var $filediff = $(node).closest('.filediff');
564 if (show === true) {
581 if (show === true) {
565 $filediff.removeClass('hide-comments');
582 $filediff.removeClass('hide-comments');
566 } else if (show === false) {
583 } else if (show === false) {
567 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
584 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
568 $filediff.addClass('hide-comments');
585 $filediff.addClass('hide-comments');
569 } else {
586 } else {
570 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
587 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
571 $filediff.toggleClass('hide-comments');
588 $filediff.toggleClass('hide-comments');
572 }
589 }
573 return false;
590 return false;
574 };
591 };
575
592
576 this.toggleLineComments = function(node) {
593 this.toggleLineComments = function(node) {
577 self.toggleComments(node, true);
594 self.toggleComments(node, true);
578 var $node = $(node);
595 var $node = $(node);
579 $node.closest('tr').toggleClass('hide-line-comments');
596 $node.closest('tr').toggleClass('hide-line-comments');
580 };
597 };
581
598
582 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
599 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
583 var pullRequestId = templateContext.pull_request_data.pull_request_id;
600 var pullRequestId = templateContext.pull_request_data.pull_request_id;
584 var commitId = templateContext.commit_data.commit_id;
601 var commitId = templateContext.commit_data.commit_id;
585
602
586 var commentForm = new CommentForm(
603 var commentForm = new CommentForm(
587 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
604 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
588 var cm = commentForm.getCmInstance();
605 var cm = commentForm.getCmInstance();
589
606
590 if (resolvesCommentId){
607 if (resolvesCommentId){
591 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
608 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
592 }
609 }
593
610
594 setTimeout(function() {
611 setTimeout(function() {
595 // callbacks
612 // callbacks
596 if (cm !== undefined) {
613 if (cm !== undefined) {
597 commentForm.setPlaceholder(placeholderText);
614 commentForm.setPlaceholder(placeholderText);
598 if (commentForm.isInline()) {
615 if (commentForm.isInline()) {
599 cm.focus();
616 cm.focus();
600 cm.refresh();
617 cm.refresh();
601 }
618 }
602 }
619 }
603 }, 10);
620 }, 10);
604
621
605 // trigger scrolldown to the resolve comment, since it might be away
622 // trigger scrolldown to the resolve comment, since it might be away
606 // from the clicked
623 // from the clicked
607 if (resolvesCommentId){
624 if (resolvesCommentId){
608 var actionNode = $(commentForm.resolvesActionId).offset();
625 var actionNode = $(commentForm.resolvesActionId).offset();
609
626
610 setTimeout(function() {
627 setTimeout(function() {
611 if (actionNode) {
628 if (actionNode) {
612 $('body, html').animate({scrollTop: actionNode.top}, 10);
629 $('body, html').animate({scrollTop: actionNode.top}, 10);
613 }
630 }
614 }, 100);
631 }, 100);
615 }
632 }
616
633
617 return commentForm;
634 return commentForm;
618 };
635 };
619
636
620 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
637 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
621
638
622 var tmpl = $('#cb-comment-general-form-template').html();
639 var tmpl = $('#cb-comment-general-form-template').html();
623 tmpl = tmpl.format(null, 'general');
640 tmpl = tmpl.format(null, 'general');
624 var $form = $(tmpl);
641 var $form = $(tmpl);
625
642
626 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
643 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
627 var curForm = $formPlaceholder.find('form');
644 var curForm = $formPlaceholder.find('form');
628 if (curForm){
645 if (curForm){
629 curForm.remove();
646 curForm.remove();
630 }
647 }
631 $formPlaceholder.append($form);
648 $formPlaceholder.append($form);
632
649
633 var _form = $($form[0]);
650 var _form = $($form[0]);
634 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
651 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
635 var commentForm = this.createCommentForm(
652 var commentForm = this.createCommentForm(
636 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
653 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
637 commentForm.initStatusChangeSelector();
654 commentForm.initStatusChangeSelector();
638
655
639 return commentForm;
656 return commentForm;
640 };
657 };
641
658
642 this.createComment = function(node, resolutionComment) {
659 this.createComment = function(node, resolutionComment) {
643 var resolvesCommentId = resolutionComment || null;
660 var resolvesCommentId = resolutionComment || null;
644 var $node = $(node);
661 var $node = $(node);
645 var $td = $node.closest('td');
662 var $td = $node.closest('td');
646 var $form = $td.find('.comment-inline-form');
663 var $form = $td.find('.comment-inline-form');
647
664
648 if (!$form.length) {
665 if (!$form.length) {
649
666
650 var $filediff = $node.closest('.filediff');
667 var $filediff = $node.closest('.filediff');
651 $filediff.removeClass('hide-comments');
668 $filediff.removeClass('hide-comments');
652 var f_path = $filediff.attr('data-f-path');
669 var f_path = $filediff.attr('data-f-path');
653 var lineno = self.getLineNumber(node);
670 var lineno = self.getLineNumber(node);
654 // create a new HTML from template
671 // create a new HTML from template
655 var tmpl = $('#cb-comment-inline-form-template').html();
672 var tmpl = $('#cb-comment-inline-form-template').html();
656 tmpl = tmpl.format(f_path, lineno);
673 tmpl = tmpl.format(f_path, lineno);
657 $form = $(tmpl);
674 $form = $(tmpl);
658
675
659 var $comments = $td.find('.inline-comments');
676 var $comments = $td.find('.inline-comments');
660 if (!$comments.length) {
677 if (!$comments.length) {
661 $comments = $(
678 $comments = $(
662 $('#cb-comments-inline-container-template').html());
679 $('#cb-comments-inline-container-template').html());
663 $td.append($comments);
680 $td.append($comments);
664 }
681 }
665
682
666 $td.find('.cb-comment-add-button').before($form);
683 $td.find('.cb-comment-add-button').before($form);
667
684
668 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
685 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
669 var _form = $($form[0]).find('form');
686 var _form = $($form[0]).find('form');
670 var autocompleteActions = ['as_note', 'as_todo'];
687 var autocompleteActions = ['as_note', 'as_todo'];
671 var commentForm = this.createCommentForm(
688 var commentForm = this.createCommentForm(
672 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
689 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
673
690
674 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
691 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
675 form: _form,
692 form: _form,
676 parent: $td[0],
693 parent: $td[0],
677 lineno: lineno,
694 lineno: lineno,
678 f_path: f_path}
695 f_path: f_path}
679 );
696 );
680
697
681 // set a CUSTOM submit handler for inline comments.
698 // set a CUSTOM submit handler for inline comments.
682 commentForm.setHandleFormSubmit(function(o) {
699 commentForm.setHandleFormSubmit(function(o) {
683 var text = commentForm.cm.getValue();
700 var text = commentForm.cm.getValue();
684 var commentType = commentForm.getCommentType();
701 var commentType = commentForm.getCommentType();
685 var resolvesCommentId = commentForm.getResolvesId();
702 var resolvesCommentId = commentForm.getResolvesId();
686
703
687 if (text === "") {
704 if (text === "") {
688 return;
705 return;
689 }
706 }
690
707
691 if (lineno === undefined) {
708 if (lineno === undefined) {
692 alert('missing line !');
709 alert('missing line !');
693 return;
710 return;
694 }
711 }
695 if (f_path === undefined) {
712 if (f_path === undefined) {
696 alert('missing file path !');
713 alert('missing file path !');
697 return;
714 return;
698 }
715 }
699
716
700 var excludeCancelBtn = false;
717 var excludeCancelBtn = false;
701 var submitEvent = true;
718 var submitEvent = true;
702 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
719 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
703 commentForm.cm.setOption("readOnly", true);
720 commentForm.cm.setOption("readOnly", true);
704 var postData = {
721 var postData = {
705 'text': text,
722 'text': text,
706 'f_path': f_path,
723 'f_path': f_path,
707 'line': lineno,
724 'line': lineno,
708 'comment_type': commentType,
725 'comment_type': commentType,
709 'csrf_token': CSRF_TOKEN
726 'csrf_token': CSRF_TOKEN
710 };
727 };
711 if (resolvesCommentId){
728 if (resolvesCommentId){
712 postData['resolves_comment_id'] = resolvesCommentId;
729 postData['resolves_comment_id'] = resolvesCommentId;
713 }
730 }
714
731
715 var submitSuccessCallback = function(json_data) {
732 var submitSuccessCallback = function(json_data) {
716 $form.remove();
733 $form.remove();
717 try {
734 try {
718 var html = json_data.rendered_text;
735 var html = json_data.rendered_text;
719 var lineno = json_data.line_no;
736 var lineno = json_data.line_no;
720 var target_id = json_data.target_id;
737 var target_id = json_data.target_id;
721
738
722 $comments.find('.cb-comment-add-button').before(html);
739 $comments.find('.cb-comment-add-button').before(html);
723
740
724 //mark visually which comment was resolved
741 //mark visually which comment was resolved
725 if (resolvesCommentId) {
742 if (resolvesCommentId) {
726 commentForm.markCommentResolved(resolvesCommentId);
743 commentForm.markCommentResolved(resolvesCommentId);
727 }
744 }
728
745
729 // run global callback on submit
746 // run global callback on submit
730 commentForm.globalSubmitSuccessCallback();
747 commentForm.globalSubmitSuccessCallback();
731
748
732 } catch (e) {
749 } catch (e) {
733 console.error(e);
750 console.error(e);
734 }
751 }
735
752
736 // re trigger the linkification of next/prev navigation
753 // re trigger the linkification of next/prev navigation
737 linkifyComments($('.inline-comment-injected'));
754 linkifyComments($('.inline-comment-injected'));
738 timeagoActivate();
755 timeagoActivate();
739 commentForm.setActionButtonsDisabled(false);
756 commentForm.setActionButtonsDisabled(false);
740
757
741 };
758 };
742 var submitFailCallback = function(){
759 var submitFailCallback = function(){
743 commentForm.resetCommentFormState(text)
760 commentForm.resetCommentFormState(text)
744 };
761 };
745 commentForm.submitAjaxPOST(
762 commentForm.submitAjaxPOST(
746 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
763 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
747 });
764 });
748 }
765 }
749
766
750 $form.addClass('comment-inline-form-open');
767 $form.addClass('comment-inline-form-open');
751 };
768 };
752
769
753 this.createResolutionComment = function(commentId){
770 this.createResolutionComment = function(commentId){
754 // hide the trigger text
771 // hide the trigger text
755 $('#resolve-comment-{0}'.format(commentId)).hide();
772 $('#resolve-comment-{0}'.format(commentId)).hide();
756
773
757 var comment = $('#comment-'+commentId);
774 var comment = $('#comment-'+commentId);
758 var commentData = comment.data();
775 var commentData = comment.data();
759 if (commentData.commentInline) {
776 if (commentData.commentInline) {
760 this.createComment(comment, commentId)
777 this.createComment(comment, commentId)
761 } else {
778 } else {
762 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
779 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
763 }
780 }
764
781
765 return false;
782 return false;
766 };
783 };
767
784
768 this.submitResolution = function(commentId){
785 this.submitResolution = function(commentId){
769 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
786 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
770 var commentForm = form.get(0).CommentForm;
787 var commentForm = form.get(0).CommentForm;
771
788
772 var cm = commentForm.getCmInstance();
789 var cm = commentForm.getCmInstance();
773 var renderer = templateContext.visual.default_renderer;
790 var renderer = templateContext.visual.default_renderer;
774 if (renderer == 'rst'){
791 if (renderer == 'rst'){
775 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
792 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
776 } else if (renderer == 'markdown') {
793 } else if (renderer == 'markdown') {
777 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
794 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
778 } else {
795 } else {
779 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
796 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
780 }
797 }
781
798
782 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
799 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
783 form.submit();
800 form.submit();
784 return false;
801 return false;
785 };
802 };
786
803
787 this.renderInlineComments = function(file_comments) {
804 this.renderInlineComments = function(file_comments) {
788 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
805 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
789
806
790 for (var i = 0; i < file_comments.length; i++) {
807 for (var i = 0; i < file_comments.length; i++) {
791 var box = file_comments[i];
808 var box = file_comments[i];
792
809
793 var target_id = $(box).attr('target_id');
810 var target_id = $(box).attr('target_id');
794
811
795 // actually comments with line numbers
812 // actually comments with line numbers
796 var comments = box.children;
813 var comments = box.children;
797
814
798 for (var j = 0; j < comments.length; j++) {
815 for (var j = 0; j < comments.length; j++) {
799 var data = {
816 var data = {
800 'rendered_text': comments[j].outerHTML,
817 'rendered_text': comments[j].outerHTML,
801 'line_no': $(comments[j]).attr('line'),
818 'line_no': $(comments[j]).attr('line'),
802 'target_id': target_id
819 'target_id': target_id
803 };
820 };
804 }
821 }
805 }
822 }
806
823
807 // since order of injection is random, we're now re-iterating
824 // since order of injection is random, we're now re-iterating
808 // from correct order and filling in links
825 // from correct order and filling in links
809 linkifyComments($('.inline-comment-injected'));
826 linkifyComments($('.inline-comment-injected'));
810 firefoxAnchorFix();
827 firefoxAnchorFix();
811 };
828 };
812
829
813 };
830 };
@@ -1,395 +1,400 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%def name="comment_block(comment, inline=False)">
8 <%def name="comment_block(comment, inline=False)">
9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 % if inline:
10 % if inline:
11 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
11 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
12 % else:
12 % else:
13 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
13 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
14 % endif
14 % endif
15
15
16
16
17 <div class="comment
17 <div class="comment
18 ${'comment-inline' if inline else 'comment-general'}
18 ${'comment-inline' if inline else 'comment-general'}
19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
20 id="comment-${comment.comment_id}"
20 id="comment-${comment.comment_id}"
21 line="${comment.line_no}"
21 line="${comment.line_no}"
22 data-comment-id="${comment.comment_id}"
22 data-comment-id="${comment.comment_id}"
23 data-comment-type="${comment.comment_type}"
23 data-comment-type="${comment.comment_type}"
24 data-comment-inline=${h.json.dumps(inline)}
24 data-comment-inline=${h.json.dumps(inline)}
25 style="${'display: none;' if outdated_at_ver else ''}">
25 style="${'display: none;' if outdated_at_ver else ''}">
26
26
27 <div class="meta">
27 <div class="meta">
28 <div class="comment-type-label">
28 <div class="comment-type-label">
29 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
29 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
30 % if comment.comment_type == 'todo':
30 % if comment.comment_type == 'todo':
31 % if comment.resolved:
31 % if comment.resolved:
32 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
32 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
33 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
33 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
34 </div>
34 </div>
35 % else:
35 % else:
36 <div class="resolved tooltip" style="display: none">
36 <div class="resolved tooltip" style="display: none">
37 <span>${comment.comment_type}</span>
37 <span>${comment.comment_type}</span>
38 </div>
38 </div>
39 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
39 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
40 ${comment.comment_type}
40 ${comment.comment_type}
41 </div>
41 </div>
42 % endif
42 % endif
43 % else:
43 % else:
44 % if comment.resolved_comment:
44 % if comment.resolved_comment:
45 fix
45 fix
46 % else:
46 % else:
47 ${comment.comment_type or 'note'}
47 ${comment.comment_type or 'note'}
48 % endif
48 % endif
49 % endif
49 % endif
50 </div>
50 </div>
51 </div>
51 </div>
52
52
53 <div class="author ${'author-inline' if inline else 'author-general'}">
53 <div class="author ${'author-inline' if inline else 'author-general'}">
54 ${base.gravatar_with_user(comment.author.email, 16)}
54 ${base.gravatar_with_user(comment.author.email, 16)}
55 </div>
55 </div>
56 <div class="date">
56 <div class="date">
57 ${h.age_component(comment.modified_at, time_is_local=True)}
57 ${h.age_component(comment.modified_at, time_is_local=True)}
58 </div>
58 </div>
59 % if inline:
59 % if inline:
60 <span></span>
60 <span></span>
61 % else:
61 % else:
62 <div class="status-change">
62 <div class="status-change">
63 % if comment.pull_request:
63 % if comment.pull_request:
64 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
64 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
65 % if comment.status_change:
65 % if comment.status_change:
66 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
66 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
67 % else:
67 % else:
68 ${_('pull request #%s') % comment.pull_request.pull_request_id}
68 ${_('pull request #%s') % comment.pull_request.pull_request_id}
69 % endif
69 % endif
70 </a>
70 </a>
71 % else:
71 % else:
72 % if comment.status_change:
72 % if comment.status_change:
73 ${_('Status change on commit')}:
73 ${_('Status change on commit')}:
74 % endif
74 % endif
75 % endif
75 % endif
76 </div>
76 </div>
77 % endif
77 % endif
78
78
79 % if comment.status_change:
79 % if comment.status_change:
80 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
80 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
81 <div title="${_('Commit status')}" class="changeset-status-lbl">
81 <div title="${_('Commit status')}" class="changeset-status-lbl">
82 ${comment.status_change[0].status_lbl}
82 ${comment.status_change[0].status_lbl}
83 </div>
83 </div>
84 % endif
84 % endif
85
85
86 % if comment.resolved_comment:
86 % if comment.resolved_comment:
87 <a class="has-spacer-before" 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)})">
87 <a class="has-spacer-before" 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)})">
88 ${_('resolves comment #{}').format(comment.resolved_comment.comment_id)}
88 ${_('resolves comment #{}').format(comment.resolved_comment.comment_id)}
89 </a>
89 </a>
90 % endif
90 % endif
91
91
92 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
92 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
93
93
94 <div class="comment-links-block">
94 <div class="comment-links-block">
95
95
96 % if inline:
96 % if inline:
97 <div class="pr-version-inline">
97 <div class="pr-version-inline">
98 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
98 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
99 % if outdated_at_ver:
99 % if outdated_at_ver:
100 <code class="pr-version-num" title="${_('Outdated comment from pull request version {0}').format(pr_index_ver)}">
100 <code class="pr-version-num" title="${_('Outdated comment from pull request version {0}').format(pr_index_ver)}">
101 outdated ${'v{}'.format(pr_index_ver)} |
101 outdated ${'v{}'.format(pr_index_ver)} |
102 </code>
102 </code>
103 % elif pr_index_ver:
103 % elif pr_index_ver:
104 <code class="pr-version-num" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
104 <code class="pr-version-num" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
105 ${'v{}'.format(pr_index_ver)} |
105 ${'v{}'.format(pr_index_ver)} |
106 </code>
106 </code>
107 % endif
107 % endif
108 </a>
108 </a>
109 </div>
109 </div>
110 % else:
110 % else:
111 % if comment.pull_request_version_id and pr_index_ver:
111 % if comment.pull_request_version_id and pr_index_ver:
112 |
112 |
113 <div class="pr-version">
113 <div class="pr-version">
114 % if comment.outdated:
114 % if comment.outdated:
115 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
115 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
116 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
116 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
117 </a>
117 </a>
118 % else:
118 % else:
119 <div title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
119 <div title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
120 <a href="${h.url('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)}">
120 <a href="${h.url('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)}">
121 <code class="pr-version-num">
121 <code class="pr-version-num">
122 ${'v{}'.format(pr_index_ver)}
122 ${'v{}'.format(pr_index_ver)}
123 </code>
123 </code>
124 </a>
124 </a>
125 </div>
125 </div>
126 % endif
126 % endif
127 </div>
127 </div>
128 % endif
128 % endif
129 % endif
129 % endif
130
130
131 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
131 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
132 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
132 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
133 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
133 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
134 ## permissions to delete
134 ## permissions to delete
135 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
135 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
136 ## TODO: dan: add edit comment here
136 ## TODO: dan: add edit comment here
137 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
137 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
138 %else:
138 %else:
139 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
139 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
140 %endif
140 %endif
141 %else:
141 %else:
142 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
142 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
143 %endif
143 %endif
144
144
145 % if outdated_at_ver:
145 % if outdated_at_ver:
146 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
146 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
147 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
147 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
148 % else:
148 % else:
149 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
149 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
150 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
150 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
151 % endif
151 % endif
152
152
153 </div>
153 </div>
154 </div>
154 </div>
155 <div class="text">
155 <div class="text">
156 ${comment.render(mentions=True)|n}
156 ${comment.render(mentions=True)|n}
157 </div>
157 </div>
158
158
159 </div>
159 </div>
160 </%def>
160 </%def>
161
161
162 ## generate main comments
162 ## generate main comments
163 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
163 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
164 <div class="general-comments" id="comments">
164 <div class="general-comments" id="comments">
165 %for comment in comments:
165 %for comment in comments:
166 <div id="comment-tr-${comment.comment_id}">
166 <div id="comment-tr-${comment.comment_id}">
167 ## only render comments that are not from pull request, or from
167 ## only render comments that are not from pull request, or from
168 ## pull request and a status change
168 ## pull request and a status change
169 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
169 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
170 ${comment_block(comment)}
170 ${comment_block(comment)}
171 %endif
171 %endif
172 </div>
172 </div>
173 %endfor
173 %endfor
174 ## to anchor ajax comments
174 ## to anchor ajax comments
175 <div id="injected_page_comments"></div>
175 <div id="injected_page_comments"></div>
176 </div>
176 </div>
177 </%def>
177 </%def>
178
178
179
179
180 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
180 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
181
181
182 <div class="comments">
182 <div class="comments">
183 <%
183 <%
184 if is_pull_request:
184 if is_pull_request:
185 placeholder = _('Leave a comment on this Pull Request.')
185 placeholder = _('Leave a comment on this Pull Request.')
186 elif is_compare:
186 elif is_compare:
187 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
187 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
188 else:
188 else:
189 placeholder = _('Leave a comment on this Commit.')
189 placeholder = _('Leave a comment on this Commit.')
190 %>
190 %>
191
191
192 % if c.rhodecode_user.username != h.DEFAULT_USER:
192 % if c.rhodecode_user.username != h.DEFAULT_USER:
193 <div class="js-template" id="cb-comment-general-form-template">
193 <div class="js-template" id="cb-comment-general-form-template">
194 ## template generated for injection
194 ## template generated for injection
195 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
195 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
196 </div>
196 </div>
197
197
198 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
198 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
199 ## inject form here
199 ## inject form here
200 </div>
200 </div>
201 <script type="text/javascript">
201 <script type="text/javascript">
202 var lineNo = 'general';
202 var lineNo = 'general';
203 var resolvesCommentId = null;
203 var resolvesCommentId = null;
204 var generalCommentForm = Rhodecode.comments.createGeneralComment(
204 var generalCommentForm = Rhodecode.comments.createGeneralComment(
205 lineNo, "${placeholder}", resolvesCommentId);
205 lineNo, "${placeholder}", resolvesCommentId);
206
206
207 // set custom success callback on rangeCommit
207 // set custom success callback on rangeCommit
208 % if is_compare:
208 % if is_compare:
209 generalCommentForm.setHandleFormSubmit(function(o) {
209 generalCommentForm.setHandleFormSubmit(function(o) {
210 var self = generalCommentForm;
210 var self = generalCommentForm;
211
211
212 var text = self.cm.getValue();
212 var text = self.cm.getValue();
213 var status = self.getCommentStatus();
213 var status = self.getCommentStatus();
214 var commentType = self.getCommentType();
214 var commentType = self.getCommentType();
215
215
216 if (text === "" && !status) {
216 if (text === "" && !status) {
217 return;
217 return;
218 }
218 }
219
219
220 // we can pick which commits we want to make the comment by
220 // we can pick which commits we want to make the comment by
221 // selecting them via click on preview pane, this will alter the hidden inputs
221 // selecting them via click on preview pane, this will alter the hidden inputs
222 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
222 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
223
223
224 var commitIds = [];
224 var commitIds = [];
225 $('#changeset_compare_view_content .compare_select').each(function(el) {
225 $('#changeset_compare_view_content .compare_select').each(function(el) {
226 var commitId = this.id.replace('row-', '');
226 var commitId = this.id.replace('row-', '');
227 if ($(this).hasClass('hl') || !cherryPicked) {
227 if ($(this).hasClass('hl') || !cherryPicked) {
228 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
228 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
229 commitIds.push(commitId);
229 commitIds.push(commitId);
230 } else {
230 } else {
231 $("input[data-commit-id='{0}']".format(commitId)).val('')
231 $("input[data-commit-id='{0}']".format(commitId)).val('')
232 }
232 }
233 });
233 });
234
234
235 self.setActionButtonsDisabled(true);
235 self.setActionButtonsDisabled(true);
236 self.cm.setOption("readOnly", true);
236 self.cm.setOption("readOnly", true);
237 var postData = {
237 var postData = {
238 'text': text,
238 'text': text,
239 'changeset_status': status,
239 'changeset_status': status,
240 'comment_type': commentType,
240 'comment_type': commentType,
241 'commit_ids': commitIds,
241 'commit_ids': commitIds,
242 'csrf_token': CSRF_TOKEN
242 'csrf_token': CSRF_TOKEN
243 };
243 };
244
244
245 var submitSuccessCallback = function(o) {
245 var submitSuccessCallback = function(o) {
246 location.reload(true);
246 location.reload(true);
247 };
247 };
248 var submitFailCallback = function(){
248 var submitFailCallback = function(){
249 self.resetCommentFormState(text)
249 self.resetCommentFormState(text)
250 };
250 };
251 self.submitAjaxPOST(
251 self.submitAjaxPOST(
252 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
252 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
253 });
253 });
254 % endif
254 % endif
255
255
256
256
257 </script>
257 </script>
258 % else:
258 % else:
259 ## form state when not logged in
259 ## form state when not logged in
260 <div class="comment-form ac">
260 <div class="comment-form ac">
261
261
262 <div class="comment-area">
262 <div class="comment-area">
263 <div class="comment-area-header">
263 <div class="comment-area-header">
264 <ul class="nav-links clearfix">
264 <ul class="nav-links clearfix">
265 <li class="active">
265 <li class="active">
266 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
266 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
267 </li>
267 </li>
268 <li class="">
268 <li class="">
269 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
269 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
270 </li>
270 </li>
271 </ul>
271 </ul>
272 </div>
272 </div>
273
273
274 <div class="comment-area-write" style="display: block;">
274 <div class="comment-area-write" style="display: block;">
275 <div id="edit-container">
275 <div id="edit-container">
276 <div style="padding: 40px 0">
276 <div style="padding: 40px 0">
277 ${_('You need to be logged in to leave comments.')}
277 ${_('You need to be logged in to leave comments.')}
278 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
278 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
279 </div>
279 </div>
280 </div>
280 </div>
281 <div id="preview-container" class="clearfix" style="display: none;">
281 <div id="preview-container" class="clearfix" style="display: none;">
282 <div id="preview-box" class="preview-box"></div>
282 <div id="preview-box" class="preview-box"></div>
283 </div>
283 </div>
284 </div>
284 </div>
285
285
286 <div class="comment-area-footer">
286 <div class="comment-area-footer">
287 <div class="toolbar">
287 <div class="toolbar">
288 <div class="toolbar-text">
288 <div class="toolbar-text">
289 </div>
289 </div>
290 </div>
290 </div>
291 </div>
291 </div>
292 </div>
292 </div>
293
293
294 <div class="comment-footer">
294 <div class="comment-footer">
295 </div>
295 </div>
296
296
297 </div>
297 </div>
298 % endif
298 % endif
299
299
300 <script type="text/javascript">
300 <script type="text/javascript">
301 bindToggleButtons();
301 bindToggleButtons();
302 </script>
302 </script>
303 </div>
303 </div>
304 </%def>
304 </%def>
305
305
306
306
307 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
307 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
308 ## comment injected based on assumption that user is logged in
308 ## comment injected based on assumption that user is logged in
309
309
310 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
310 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
311
311
312 <div class="comment-area">
312 <div class="comment-area">
313 <div class="comment-area-header">
313 <div class="comment-area-header">
314 <ul class="nav-links clearfix">
314 <ul class="nav-links clearfix">
315 <li class="active">
315 <li class="active">
316 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
316 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
317 </li>
317 </li>
318 <li class="">
318 <li class="">
319 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
319 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
320 </li>
320 </li>
321 <li class="pull-right">
321 <li class="pull-right">
322 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
322 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
323 % for val in c.visual.comment_types:
323 % for val in c.visual.comment_types:
324 <option value="${val}">${val.upper()}</option>
324 <option value="${val}">${val.upper()}</option>
325 % endfor
325 % endfor
326 </select>
326 </select>
327 </li>
327 </li>
328 </ul>
328 </ul>
329 </div>
329 </div>
330
330
331 <div class="comment-area-write" style="display: block;">
331 <div class="comment-area-write" style="display: block;">
332 <div id="edit-container_${lineno_id}">
332 <div id="edit-container_${lineno_id}">
333 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
333 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
334 </div>
334 </div>
335 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
335 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
336 <div id="preview-box_${lineno_id}" class="preview-box"></div>
336 <div id="preview-box_${lineno_id}" class="preview-box"></div>
337 </div>
337 </div>
338 </div>
338 </div>
339
339
340 <div class="comment-area-footer">
340 <div class="comment-area-footer">
341 <div class="toolbar">
341 <div class="toolbar">
342 <div class="toolbar-text">
342 <div class="toolbar-text">
343 ${(_('Comments parsed using %s syntax with %s, and %s actions support.') % (
343 ${(_('Comments parsed using %s syntax with %s, and %s actions support.') % (
344 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
344 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
345 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user')),
345 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user')),
346 ('<span class="tooltip" title="%s">`/`</span>' % _('Start typing with / for certain actions to be triggered via text box.'))
346 ('<span class="tooltip" title="%s">`/`</span>' % _('Start typing with / for certain actions to be triggered via text box.'))
347 )
347 )
348 )|n}
348 )|n}
349 </div>
349 </div>
350 </div>
350 </div>
351 </div>
351 </div>
352 </div>
352 </div>
353
353
354 <div class="comment-footer">
354 <div class="comment-footer">
355
355
356 % if review_statuses:
356 % if review_statuses:
357 <div class="status_box">
357 <div class="status_box">
358 <select id="change_status_${lineno_id}" name="changeset_status">
358 <select id="change_status_${lineno_id}" name="changeset_status">
359 <option></option> ## Placeholder
359 <option></option> ## Placeholder
360 % for status, lbl in review_statuses:
360 % for status, lbl in review_statuses:
361 <option value="${status}" data-status="${status}">${lbl}</option>
361 <option value="${status}" data-status="${status}">${lbl}</option>
362 %if is_pull_request and change_status and status in ('approved', 'rejected'):
362 %if is_pull_request and change_status and status in ('approved', 'rejected'):
363 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
363 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
364 %endif
364 %endif
365 % endfor
365 % endfor
366 </select>
366 </select>
367 </div>
367 </div>
368 % endif
368 % endif
369
369
370 ## inject extra inputs into the form
370 ## inject extra inputs into the form
371 % if form_extras and isinstance(form_extras, (list, tuple)):
371 % if form_extras and isinstance(form_extras, (list, tuple)):
372 <div id="comment_form_extras">
372 <div id="comment_form_extras">
373 % for form_ex_el in form_extras:
373 % for form_ex_el in form_extras:
374 ${form_ex_el|n}
374 ${form_ex_el|n}
375 % endfor
375 % endfor
376 </div>
376 </div>
377 % endif
377 % endif
378
378
379 <div class="action-buttons">
379 <div class="action-buttons">
380 ## inline for has a file, and line-number together with cancel hide button.
380 ## inline for has a file, and line-number together with cancel hide button.
381 % if form_type == 'inline':
381 % if form_type == 'inline':
382 <input type="hidden" name="f_path" value="{0}">
382 <input type="hidden" name="f_path" value="{0}">
383 <input type="hidden" name="line" value="${lineno_id}">
383 <input type="hidden" name="line" value="${lineno_id}">
384 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
384 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
385 ${_('Cancel')}
385 ${_('Cancel')}
386 </button>
386 </button>
387 % endif
387 % endif
388
389 % if form_type != 'inline':
390 <div class="action-buttons-extra"></div>
391 % endif
392
388 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
393 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
389
394
390 </div>
395 </div>
391 </div>
396 </div>
392
397
393 </form>
398 </form>
394
399
395 </%def> No newline at end of file
400 </%def>
@@ -1,50 +1,63 b''
1
1
2 <div class="pull-request-wrap">
2 <div class="pull-request-wrap">
3
3
4
5 % if c.pr_merge_possible:
4 % if c.pr_merge_possible:
6 <h2 class="merge-status">
5 <h2 class="merge-status">
7 <span class="merge-icon success"><i class="icon-true"></i></span>
6 <span class="merge-icon success"><i class="icon-true"></i></span>
8 ${_('This pull request can be merged automatically.')}
7 ${_('This pull request can be merged automatically.')}
9 </h2>
8 </h2>
10 % else:
9 % else:
11 <h2 class="merge-status">
10 <h2 class="merge-status">
12 <span class="merge-icon warning"><i class="icon-false"></i></span>
11 <span class="merge-icon warning"><i class="icon-false"></i></span>
13 ${_('Merge is not currently possible because of below failed checks.')}
12 ${_('Merge is not currently possible because of below failed checks.')}
14 </h2>
13 </h2>
15 % endif
14 % endif
16
15
17 <ul>
16 <ul>
18 % for pr_check_key, pr_check_details in c.pr_merge_errors.items():
17 % for pr_check_key, pr_check_details in c.pr_merge_errors.items():
19 <% pr_check_type = pr_check_details['error_type'] %>
18 <% pr_check_type = pr_check_details['error_type'] %>
20 <li>
19 <li>
21 <span class="merge-message ${pr_check_type}" data-role="merge-message">
20 <span class="merge-message ${pr_check_type}" data-role="merge-message">
22 - ${pr_check_details['message']}
21 - ${pr_check_details['message']}
23 % if pr_check_key == 'todo':
22 % if pr_check_key == 'todo':
24 % for co in pr_check_details['details']:
23 % for co in pr_check_details['details']:
25 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'), 0, ${h.json.dumps(co.outdated)})"> #${co.comment_id}</a>${'' if loop.last else ','}
24 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'), 0, ${h.json.dumps(co.outdated)})"> #${co.comment_id}</a>${'' if loop.last else ','}
26 % endfor
25 % endfor
27 % endif
26 % endif
28 </span>
27 </span>
29 </li>
28 </li>
30 % endfor
29 % endfor
31 </ul>
30 </ul>
32
31
33 <div class="pull-request-merge-actions">
32 <div class="pull-request-merge-actions">
34 % if c.allowed_to_merge:
33 % if c.allowed_to_merge:
35 <div class="pull-right">
34 <div class="pull-right">
36 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
35 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
37 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
36 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
38 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
37 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
39 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
38 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
40 ${h.end_form()}
39 ${h.end_form()}
41 </div>
40 </div>
42 % elif c.rhodecode_user.username != h.DEFAULT_USER:
41 % elif c.rhodecode_user.username != h.DEFAULT_USER:
43 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
42 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
44 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
43 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
45 % else:
44 % else:
46 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
45 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
47 % endif
46 % endif
48 </div>
47 </div>
48
49 % if c.allowed_to_close:
50 ## close PR action, injected later next to COMMENT button
51 <div id="close-pull-request-action" style="display: none">
52 % if c.pull_request_review_status == c.REVIEW_STATUS_APPROVED:
53 <a class="btn btn-approved-status" href="#close-as-approved" onclick="closePullRequest('${c.REVIEW_STATUS_APPROVED}'); return false;">
54 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_APPROVED))}
55 </a>
56 % else:
57 <a class="btn btn-rejected-status" href="#close-as-rejected" onclick="closePullRequest('${c.REVIEW_STATUS_REJECTED}'); return false;">
58 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_REJECTED))}
59 </a>
60 % endif
61 </div>
62 % endif
49 </div>
63 </div>
50
@@ -1,796 +1,818 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 %if c.rhodecode_name:
6 %if c.rhodecode_name:
7 &middot; ${h.branding(c.rhodecode_name)}
7 &middot; ${h.branding(c.rhodecode_name)}
8 %endif
8 %endif
9 </%def>
9 </%def>
10
10
11 <%def name="breadcrumbs_links()">
11 <%def name="breadcrumbs_links()">
12 <span id="pr-title">
12 <span id="pr-title">
13 ${c.pull_request.title}
13 ${c.pull_request.title}
14 %if c.pull_request.is_closed():
14 %if c.pull_request.is_closed():
15 (${_('Closed')})
15 (${_('Closed')})
16 %endif
16 %endif
17 </span>
17 </span>
18 <div id="pr-title-edit" class="input" style="display: none;">
18 <div id="pr-title-edit" class="input" style="display: none;">
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 </div>
20 </div>
21 </%def>
21 </%def>
22
22
23 <%def name="menu_bar_nav()">
23 <%def name="menu_bar_nav()">
24 ${self.menu_items(active='repositories')}
24 ${self.menu_items(active='repositories')}
25 </%def>
25 </%def>
26
26
27 <%def name="menu_bar_subnav()">
27 <%def name="menu_bar_subnav()">
28 ${self.repo_menu(active='showpullrequest')}
28 ${self.repo_menu(active='showpullrequest')}
29 </%def>
29 </%def>
30
30
31 <%def name="main()">
31 <%def name="main()">
32
32
33 <script type="text/javascript">
33 <script type="text/javascript">
34 // TODO: marcink switch this to pyroutes
34 // TODO: marcink switch this to pyroutes
35 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 </script>
37 </script>
38 <div class="box">
38 <div class="box">
39
39
40 <div class="title">
40 <div class="title">
41 ${self.repo_page_title(c.rhodecode_db_repo)}
41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 </div>
42 </div>
43
43
44 ${self.breadcrumbs()}
44 ${self.breadcrumbs()}
45
45
46 <div class="box pr-summary">
46 <div class="box pr-summary">
47
47
48 <div class="summary-details block-left">
48 <div class="summary-details block-left">
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 <div class="pr-details-title">
50 <div class="pr-details-title">
51 <a href="${h.url('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)}
51 <a href="${h.url('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)}
52 %if c.allowed_to_update:
52 %if c.allowed_to_update:
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 % if c.allowed_to_delete:
54 % if c.allowed_to_delete:
55 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
55 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
57 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 ${h.end_form()}
58 ${h.end_form()}
59 % else:
59 % else:
60 ${_('Delete')}
60 ${_('Delete')}
61 % endif
61 % endif
62 </div>
62 </div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 %endif
65 %endif
66 </div>
66 </div>
67
67
68 <div id="summary" class="fields pr-details-content">
68 <div id="summary" class="fields pr-details-content">
69 <div class="field">
69 <div class="field">
70 <div class="label-summary">
70 <div class="label-summary">
71 <label>${_('Origin')}:</label>
71 <label>${_('Origin')}:</label>
72 </div>
72 </div>
73 <div class="input">
73 <div class="input">
74 <div class="pr-origininfo">
74 <div class="pr-origininfo">
75 ## branch link is only valid if it is a branch
75 ## branch link is only valid if it is a branch
76 <span class="tag">
76 <span class="tag">
77 %if c.pull_request.source_ref_parts.type == 'branch':
77 %if c.pull_request.source_ref_parts.type == 'branch':
78 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
78 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 %else:
79 %else:
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 %endif
81 %endif
82 </span>
82 </span>
83 <span class="clone-url">
83 <span class="clone-url">
84 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
84 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 </span>
85 </span>
86 </div>
86 </div>
87 <div class="pr-pullinfo">
87 <div class="pr-pullinfo">
88 %if h.is_hg(c.pull_request.source_repo):
88 %if h.is_hg(c.pull_request.source_repo):
89 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
89 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
90 %elif h.is_git(c.pull_request.source_repo):
90 %elif h.is_git(c.pull_request.source_repo):
91 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
91 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
92 %endif
92 %endif
93 </div>
93 </div>
94 </div>
94 </div>
95 </div>
95 </div>
96 <div class="field">
96 <div class="field">
97 <div class="label-summary">
97 <div class="label-summary">
98 <label>${_('Target')}:</label>
98 <label>${_('Target')}:</label>
99 </div>
99 </div>
100 <div class="input">
100 <div class="input">
101 <div class="pr-targetinfo">
101 <div class="pr-targetinfo">
102 ## branch link is only valid if it is a branch
102 ## branch link is only valid if it is a branch
103 <span class="tag">
103 <span class="tag">
104 %if c.pull_request.target_ref_parts.type == 'branch':
104 %if c.pull_request.target_ref_parts.type == 'branch':
105 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
105 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
106 %else:
106 %else:
107 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
107 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
108 %endif
108 %endif
109 </span>
109 </span>
110 <span class="clone-url">
110 <span class="clone-url">
111 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
111 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
112 </span>
112 </span>
113 </div>
113 </div>
114 </div>
114 </div>
115 </div>
115 </div>
116
116
117 ## Link to the shadow repository.
117 ## Link to the shadow repository.
118 <div class="field">
118 <div class="field">
119 <div class="label-summary">
119 <div class="label-summary">
120 <label>${_('Merge')}:</label>
120 <label>${_('Merge')}:</label>
121 </div>
121 </div>
122 <div class="input">
122 <div class="input">
123 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
123 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
124 <div class="pr-mergeinfo">
124 <div class="pr-mergeinfo">
125 %if h.is_hg(c.pull_request.target_repo):
125 %if h.is_hg(c.pull_request.target_repo):
126 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
126 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
127 %elif h.is_git(c.pull_request.target_repo):
127 %elif h.is_git(c.pull_request.target_repo):
128 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
128 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
129 %endif
129 %endif
130 </div>
130 </div>
131 % else:
131 % else:
132 <div class="">
132 <div class="">
133 ${_('Shadow repository data not available')}.
133 ${_('Shadow repository data not available')}.
134 </div>
134 </div>
135 % endif
135 % endif
136 </div>
136 </div>
137 </div>
137 </div>
138
138
139 <div class="field">
139 <div class="field">
140 <div class="label-summary">
140 <div class="label-summary">
141 <label>${_('Review')}:</label>
141 <label>${_('Review')}:</label>
142 </div>
142 </div>
143 <div class="input">
143 <div class="input">
144 %if c.pull_request_review_status:
144 %if c.pull_request_review_status:
145 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
145 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
146 <span class="changeset-status-lbl tooltip">
146 <span class="changeset-status-lbl tooltip">
147 %if c.pull_request.is_closed():
147 %if c.pull_request.is_closed():
148 ${_('Closed')},
148 ${_('Closed')},
149 %endif
149 %endif
150 ${h.commit_status_lbl(c.pull_request_review_status)}
150 ${h.commit_status_lbl(c.pull_request_review_status)}
151 </span>
151 </span>
152 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
152 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
153 %endif
153 %endif
154 </div>
154 </div>
155 </div>
155 </div>
156 <div class="field">
156 <div class="field">
157 <div class="pr-description-label label-summary">
157 <div class="pr-description-label label-summary">
158 <label>${_('Description')}:</label>
158 <label>${_('Description')}:</label>
159 </div>
159 </div>
160 <div id="pr-desc" class="input">
160 <div id="pr-desc" class="input">
161 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
161 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
162 </div>
162 </div>
163 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
163 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
164 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
164 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
165 </div>
165 </div>
166 </div>
166 </div>
167
167
168 <div class="field">
168 <div class="field">
169 <div class="label-summary">
169 <div class="label-summary">
170 <label>${_('Versions')}:</label>
170 <label>${_('Versions')}:</label>
171 </div>
171 </div>
172
172
173 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
173 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
174 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
174 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
175
175
176 <div class="pr-versions">
176 <div class="pr-versions">
177 % if c.show_version_changes:
177 % if c.show_version_changes:
178 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
178 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
179 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
179 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
180 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
180 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
181 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))}"
181 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))}"
182 data-toggle-off="${_('Hide all versions of this pull request')}">
182 data-toggle-off="${_('Hide all versions of this pull request')}">
183 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
183 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
184 </a>
184 </a>
185 <table>
185 <table>
186 ## SHOW ALL VERSIONS OF PR
186 ## SHOW ALL VERSIONS OF PR
187 <% ver_pr = None %>
187 <% ver_pr = None %>
188
188
189 % for data in reversed(list(enumerate(c.versions, 1))):
189 % for data in reversed(list(enumerate(c.versions, 1))):
190 <% ver_pos = data[0] %>
190 <% ver_pos = data[0] %>
191 <% ver = data[1] %>
191 <% ver = data[1] %>
192 <% ver_pr = ver.pull_request_version_id %>
192 <% ver_pr = ver.pull_request_version_id %>
193 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
193 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
194
194
195 <tr class="version-pr" style="display: ${display_row}">
195 <tr class="version-pr" style="display: ${display_row}">
196 <td>
196 <td>
197 <code>
197 <code>
198 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
198 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
199 </code>
199 </code>
200 </td>
200 </td>
201 <td>
201 <td>
202 <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}"/>
202 <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}"/>
203 <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}"/>
203 <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}"/>
204 </td>
204 </td>
205 <td>
205 <td>
206 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
206 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
207 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
207 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
208 </div>
208 </div>
209 </td>
209 </td>
210 <td>
210 <td>
211 % if c.at_version_num != ver_pr:
211 % if c.at_version_num != ver_pr:
212 <i class="icon-comment"></i>
212 <i class="icon-comment"></i>
213 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
213 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
214 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
214 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
215 </code>
215 </code>
216 % endif
216 % endif
217 </td>
217 </td>
218 <td>
218 <td>
219 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
219 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
220 </td>
220 </td>
221 <td>
221 <td>
222 ${h.age_component(ver.updated_on)}
222 ${h.age_component(ver.updated_on)}
223 </td>
223 </td>
224 </tr>
224 </tr>
225 % endfor
225 % endfor
226
226
227 <tr>
227 <tr>
228 <td colspan="6">
228 <td colspan="6">
229 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
229 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
230 data-label-text-locked="${_('select versions to show changes')}"
230 data-label-text-locked="${_('select versions to show changes')}"
231 data-label-text-diff="${_('show changes between versions')}"
231 data-label-text-diff="${_('show changes between versions')}"
232 data-label-text-show="${_('show pull request for this version')}"
232 data-label-text-show="${_('show pull request for this version')}"
233 >
233 >
234 ${_('select versions to show changes')}
234 ${_('select versions to show changes')}
235 </button>
235 </button>
236 </td>
236 </td>
237 </tr>
237 </tr>
238
238
239 ## show comment/inline comments summary
239 ## show comment/inline comments summary
240 <%def name="comments_summary()">
240 <%def name="comments_summary()">
241 <tr>
241 <tr>
242 <td colspan="6" class="comments-summary-td">
242 <td colspan="6" class="comments-summary-td">
243
243
244 % if c.at_version:
244 % if c.at_version:
245 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
245 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
246 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
246 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
247 ${_('Comments at this version')}:
247 ${_('Comments at this version')}:
248 % else:
248 % else:
249 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
249 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
250 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
250 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
251 ${_('Comments for this pull request')}:
251 ${_('Comments for this pull request')}:
252 % endif
252 % endif
253
253
254
254
255 %if general_comm_count_ver:
255 %if general_comm_count_ver:
256 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
256 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
257 %else:
257 %else:
258 ${_("%d General ") % general_comm_count_ver}
258 ${_("%d General ") % general_comm_count_ver}
259 %endif
259 %endif
260
260
261 %if inline_comm_count_ver:
261 %if inline_comm_count_ver:
262 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
262 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
263 %else:
263 %else:
264 , ${_("%d Inline") % inline_comm_count_ver}
264 , ${_("%d Inline") % inline_comm_count_ver}
265 %endif
265 %endif
266
266
267 %if outdated_comm_count_ver:
267 %if outdated_comm_count_ver:
268 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
268 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
269 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
269 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
270 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
270 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
271 %else:
271 %else:
272 , ${_("%d Outdated") % outdated_comm_count_ver}
272 , ${_("%d Outdated") % outdated_comm_count_ver}
273 %endif
273 %endif
274 </td>
274 </td>
275 </tr>
275 </tr>
276 </%def>
276 </%def>
277 ${comments_summary()}
277 ${comments_summary()}
278 </table>
278 </table>
279 % else:
279 % else:
280 <div class="input">
280 <div class="input">
281 ${_('Pull request versions not available')}.
281 ${_('Pull request versions not available')}.
282 </div>
282 </div>
283 <div>
283 <div>
284 <table>
284 <table>
285 ${comments_summary()}
285 ${comments_summary()}
286 </table>
286 </table>
287 </div>
287 </div>
288 % endif
288 % endif
289 </div>
289 </div>
290 </div>
290 </div>
291
291
292 <div id="pr-save" class="field" style="display: none;">
292 <div id="pr-save" class="field" style="display: none;">
293 <div class="label-summary"></div>
293 <div class="label-summary"></div>
294 <div class="input">
294 <div class="input">
295 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
295 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
296 </div>
296 </div>
297 </div>
297 </div>
298 </div>
298 </div>
299 </div>
299 </div>
300 <div>
300 <div>
301 ## AUTHOR
301 ## AUTHOR
302 <div class="reviewers-title block-right">
302 <div class="reviewers-title block-right">
303 <div class="pr-details-title">
303 <div class="pr-details-title">
304 ${_('Author')}
304 ${_('Author')}
305 </div>
305 </div>
306 </div>
306 </div>
307 <div class="block-right pr-details-content reviewers">
307 <div class="block-right pr-details-content reviewers">
308 <ul class="group_members">
308 <ul class="group_members">
309 <li>
309 <li>
310 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
310 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
311 </li>
311 </li>
312 </ul>
312 </ul>
313 </div>
313 </div>
314 ## REVIEWERS
314 ## REVIEWERS
315 <div class="reviewers-title block-right">
315 <div class="reviewers-title block-right">
316 <div class="pr-details-title">
316 <div class="pr-details-title">
317 ${_('Pull request reviewers')}
317 ${_('Pull request reviewers')}
318 %if c.allowed_to_update:
318 %if c.allowed_to_update:
319 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
319 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
320 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
320 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
321 %endif
321 %endif
322 </div>
322 </div>
323 </div>
323 </div>
324 <div id="reviewers" class="block-right pr-details-content reviewers">
324 <div id="reviewers" class="block-right pr-details-content reviewers">
325 ## members goes here !
325 ## members goes here !
326 <input type="hidden" name="__start__" value="review_members:sequence">
326 <input type="hidden" name="__start__" value="review_members:sequence">
327 <ul id="review_members" class="group_members">
327 <ul id="review_members" class="group_members">
328 %for member,reasons,status in c.pull_request_reviewers:
328 %for member,reasons,status in c.pull_request_reviewers:
329 <li id="reviewer_${member.user_id}">
329 <li id="reviewer_${member.user_id}">
330 <div class="reviewers_member">
330 <div class="reviewers_member">
331 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
331 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
332 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
332 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
333 </div>
333 </div>
334 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
334 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
335 ${self.gravatar_with_user(member.email, 16)}
335 ${self.gravatar_with_user(member.email, 16)}
336 </div>
336 </div>
337 <input type="hidden" name="__start__" value="reviewer:mapping">
337 <input type="hidden" name="__start__" value="reviewer:mapping">
338 <input type="hidden" name="__start__" value="reasons:sequence">
338 <input type="hidden" name="__start__" value="reasons:sequence">
339 %for reason in reasons:
339 %for reason in reasons:
340 <div class="reviewer_reason">- ${reason}</div>
340 <div class="reviewer_reason">- ${reason}</div>
341 <input type="hidden" name="reason" value="${reason}">
341 <input type="hidden" name="reason" value="${reason}">
342
342
343 %endfor
343 %endfor
344 <input type="hidden" name="__end__" value="reasons:sequence">
344 <input type="hidden" name="__end__" value="reasons:sequence">
345 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
345 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
346 <input type="hidden" name="__end__" value="reviewer:mapping">
346 <input type="hidden" name="__end__" value="reviewer:mapping">
347 %if c.allowed_to_update:
347 %if c.allowed_to_update:
348 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
348 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
349 <i class="icon-remove-sign" ></i>
349 <i class="icon-remove-sign" ></i>
350 </div>
350 </div>
351 %endif
351 %endif
352 </div>
352 </div>
353 </li>
353 </li>
354 %endfor
354 %endfor
355 </ul>
355 </ul>
356 <input type="hidden" name="__end__" value="review_members:sequence">
356 <input type="hidden" name="__end__" value="review_members:sequence">
357 %if not c.pull_request.is_closed():
357 %if not c.pull_request.is_closed():
358 <div id="add_reviewer_input" class='ac' style="display: none;">
358 <div id="add_reviewer_input" class='ac' style="display: none;">
359 %if c.allowed_to_update:
359 %if c.allowed_to_update:
360 <div class="reviewer_ac">
360 <div class="reviewer_ac">
361 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
361 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
362 <div id="reviewers_container"></div>
362 <div id="reviewers_container"></div>
363 </div>
363 </div>
364 <div>
364 <div>
365 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
365 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
366 </div>
366 </div>
367 %endif
367 %endif
368 </div>
368 </div>
369 %endif
369 %endif
370 </div>
370 </div>
371 </div>
371 </div>
372 </div>
372 </div>
373 <div class="box">
373 <div class="box">
374 ##DIFF
374 ##DIFF
375 <div class="table" >
375 <div class="table" >
376 <div id="changeset_compare_view_content">
376 <div id="changeset_compare_view_content">
377 ##CS
377 ##CS
378 % if c.missing_requirements:
378 % if c.missing_requirements:
379 <div class="box">
379 <div class="box">
380 <div class="alert alert-warning">
380 <div class="alert alert-warning">
381 <div>
381 <div>
382 <strong>${_('Missing requirements:')}</strong>
382 <strong>${_('Missing requirements:')}</strong>
383 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
383 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
384 </div>
384 </div>
385 </div>
385 </div>
386 </div>
386 </div>
387 % elif c.missing_commits:
387 % elif c.missing_commits:
388 <div class="box">
388 <div class="box">
389 <div class="alert alert-warning">
389 <div class="alert alert-warning">
390 <div>
390 <div>
391 <strong>${_('Missing commits')}:</strong>
391 <strong>${_('Missing commits')}:</strong>
392 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
392 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
393 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
393 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
394 </div>
394 </div>
395 </div>
395 </div>
396 </div>
396 </div>
397 % endif
397 % endif
398
398
399 <div class="compare_view_commits_title">
399 <div class="compare_view_commits_title">
400 % if not c.compare_mode:
400 % if not c.compare_mode:
401
401
402 % if c.at_version_pos:
402 % if c.at_version_pos:
403 <h4>
403 <h4>
404 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
404 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
405 </h4>
405 </h4>
406 % endif
406 % endif
407
407
408 <div class="pull-left">
408 <div class="pull-left">
409 <div class="btn-group">
409 <div class="btn-group">
410 <a
410 <a
411 class="btn"
411 class="btn"
412 href="#"
412 href="#"
413 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
413 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
414 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
414 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
415 </a>
415 </a>
416 <a
416 <a
417 class="btn"
417 class="btn"
418 href="#"
418 href="#"
419 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
419 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
420 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
420 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
421 </a>
421 </a>
422 </div>
422 </div>
423 </div>
423 </div>
424
424
425 <div class="pull-right">
425 <div class="pull-right">
426 % if c.allowed_to_update and not c.pull_request.is_closed():
426 % if c.allowed_to_update and not c.pull_request.is_closed():
427 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
427 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
428 % else:
428 % else:
429 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
429 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
430 % endif
430 % endif
431
431
432 </div>
432 </div>
433 % endif
433 % endif
434 </div>
434 </div>
435
435
436 % if not c.missing_commits:
436 % if not c.missing_commits:
437 % if c.compare_mode:
437 % if c.compare_mode:
438 % if c.at_version:
438 % if c.at_version:
439 <h4>
439 <h4>
440 ${_('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')}:
440 ${_('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')}:
441 </h4>
441 </h4>
442
442
443 <div class="subtitle-compare">
443 <div class="subtitle-compare">
444 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
444 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
445 </div>
445 </div>
446
446
447 <div class="container">
447 <div class="container">
448 <table class="rctable compare_view_commits">
448 <table class="rctable compare_view_commits">
449 <tr>
449 <tr>
450 <th></th>
450 <th></th>
451 <th>${_('Time')}</th>
451 <th>${_('Time')}</th>
452 <th>${_('Author')}</th>
452 <th>${_('Author')}</th>
453 <th>${_('Commit')}</th>
453 <th>${_('Commit')}</th>
454 <th></th>
454 <th></th>
455 <th>${_('Description')}</th>
455 <th>${_('Description')}</th>
456 </tr>
456 </tr>
457
457
458 % for c_type, commit in c.commit_changes:
458 % for c_type, commit in c.commit_changes:
459 % if c_type in ['a', 'r']:
459 % if c_type in ['a', 'r']:
460 <%
460 <%
461 if c_type == 'a':
461 if c_type == 'a':
462 cc_title = _('Commit added in displayed changes')
462 cc_title = _('Commit added in displayed changes')
463 elif c_type == 'r':
463 elif c_type == 'r':
464 cc_title = _('Commit removed in displayed changes')
464 cc_title = _('Commit removed in displayed changes')
465 else:
465 else:
466 cc_title = ''
466 cc_title = ''
467 %>
467 %>
468 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
468 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
469 <td>
469 <td>
470 <div class="commit-change-indicator color-${c_type}-border">
470 <div class="commit-change-indicator color-${c_type}-border">
471 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
471 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
472 ${c_type.upper()}
472 ${c_type.upper()}
473 </div>
473 </div>
474 </div>
474 </div>
475 </td>
475 </td>
476 <td class="td-time">
476 <td class="td-time">
477 ${h.age_component(commit.date)}
477 ${h.age_component(commit.date)}
478 </td>
478 </td>
479 <td class="td-user">
479 <td class="td-user">
480 ${base.gravatar_with_user(commit.author, 16)}
480 ${base.gravatar_with_user(commit.author, 16)}
481 </td>
481 </td>
482 <td class="td-hash">
482 <td class="td-hash">
483 <code>
483 <code>
484 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
484 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
485 r${commit.revision}:${h.short_id(commit.raw_id)}
485 r${commit.revision}:${h.short_id(commit.raw_id)}
486 </a>
486 </a>
487 ${h.hidden('revisions', commit.raw_id)}
487 ${h.hidden('revisions', commit.raw_id)}
488 </code>
488 </code>
489 </td>
489 </td>
490 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
490 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
491 <div class="show_more_col">
491 <div class="show_more_col">
492 <i class="show_more"></i>
492 <i class="show_more"></i>
493 </div>
493 </div>
494 </td>
494 </td>
495 <td class="mid td-description">
495 <td class="mid td-description">
496 <div class="log-container truncate-wrap">
496 <div class="log-container truncate-wrap">
497 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
497 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
498 ${h.urlify_commit_message(commit.message, c.repo_name)}
498 ${h.urlify_commit_message(commit.message, c.repo_name)}
499 </div>
499 </div>
500 </div>
500 </div>
501 </td>
501 </td>
502 </tr>
502 </tr>
503 % endif
503 % endif
504 % endfor
504 % endfor
505 </table>
505 </table>
506 </div>
506 </div>
507
507
508 <script>
508 <script>
509 $('.expand_commit').on('click',function(e){
509 $('.expand_commit').on('click',function(e){
510 var target_expand = $(this);
510 var target_expand = $(this);
511 var cid = target_expand.data('commitId');
511 var cid = target_expand.data('commitId');
512
512
513 if (target_expand.hasClass('open')){
513 if (target_expand.hasClass('open')){
514 $('#c-'+cid).css({
514 $('#c-'+cid).css({
515 'height': '1.5em',
515 'height': '1.5em',
516 'white-space': 'nowrap',
516 'white-space': 'nowrap',
517 'text-overflow': 'ellipsis',
517 'text-overflow': 'ellipsis',
518 'overflow':'hidden'
518 'overflow':'hidden'
519 });
519 });
520 target_expand.removeClass('open');
520 target_expand.removeClass('open');
521 }
521 }
522 else {
522 else {
523 $('#c-'+cid).css({
523 $('#c-'+cid).css({
524 'height': 'auto',
524 'height': 'auto',
525 'white-space': 'pre-line',
525 'white-space': 'pre-line',
526 'text-overflow': 'initial',
526 'text-overflow': 'initial',
527 'overflow':'visible'
527 'overflow':'visible'
528 });
528 });
529 target_expand.addClass('open');
529 target_expand.addClass('open');
530 }
530 }
531 });
531 });
532 </script>
532 </script>
533
533
534 % endif
534 % endif
535
535
536 % else:
536 % else:
537 <%include file="/compare/compare_commits.mako" />
537 <%include file="/compare/compare_commits.mako" />
538 % endif
538 % endif
539
539
540 <div class="cs_files">
540 <div class="cs_files">
541 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
541 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
542 ${cbdiffs.render_diffset_menu()}
542 ${cbdiffs.render_diffset_menu()}
543 ${cbdiffs.render_diffset(
543 ${cbdiffs.render_diffset(
544 c.diffset, use_comments=True,
544 c.diffset, use_comments=True,
545 collapse_when_files_over=30,
545 collapse_when_files_over=30,
546 disable_new_comments=not c.allowed_to_comment,
546 disable_new_comments=not c.allowed_to_comment,
547 deleted_files_comments=c.deleted_files_comments)}
547 deleted_files_comments=c.deleted_files_comments)}
548 </div>
548 </div>
549 % else:
549 % else:
550 ## skipping commits we need to clear the view for missing commits
550 ## skipping commits we need to clear the view for missing commits
551 <div style="clear:both;"></div>
551 <div style="clear:both;"></div>
552 % endif
552 % endif
553
553
554 </div>
554 </div>
555 </div>
555 </div>
556
556
557 ## template for inline comment form
557 ## template for inline comment form
558 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
558 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
559
559
560 ## render general comments
560 ## render general comments
561
561
562 <div id="comment-tr-show">
562 <div id="comment-tr-show">
563 <div class="comment">
563 <div class="comment">
564 % if general_outdated_comm_count_ver:
564 % if general_outdated_comm_count_ver:
565 <div class="meta">
565 <div class="meta">
566 % if general_outdated_comm_count_ver == 1:
566 % if general_outdated_comm_count_ver == 1:
567 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
567 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
568 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
568 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
569 % else:
569 % else:
570 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
570 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
571 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
571 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
572 % endif
572 % endif
573 </div>
573 </div>
574 % endif
574 % endif
575 </div>
575 </div>
576 </div>
576 </div>
577
577
578 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
578 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
579
579
580 % if not c.pull_request.is_closed():
580 % if not c.pull_request.is_closed():
581 ## merge status, and merge action
581 ## merge status, and merge action
582 <div class="pull-request-merge">
582 <div class="pull-request-merge">
583 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
583 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
584 </div>
584 </div>
585
585
586 ## main comment form and it status
586 ## main comment form and it status
587 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
587 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
588 pull_request_id=c.pull_request.pull_request_id),
588 pull_request_id=c.pull_request.pull_request_id),
589 c.pull_request_review_status,
589 c.pull_request_review_status,
590 is_pull_request=True, change_status=c.allowed_to_change_status)}
590 is_pull_request=True, change_status=c.allowed_to_change_status)}
591 %endif
591 %endif
592
592
593 <script type="text/javascript">
593 <script type="text/javascript">
594 if (location.hash) {
594 if (location.hash) {
595 var result = splitDelimitedHash(location.hash);
595 var result = splitDelimitedHash(location.hash);
596 var line = $('html').find(result.loc);
596 var line = $('html').find(result.loc);
597 // show hidden comments if we use location.hash
597 // show hidden comments if we use location.hash
598 if (line.hasClass('comment-general')) {
598 if (line.hasClass('comment-general')) {
599 $(line).show();
599 $(line).show();
600 } else if (line.hasClass('comment-inline')) {
600 } else if (line.hasClass('comment-inline')) {
601 $(line).show();
601 $(line).show();
602 var $cb = $(line).closest('.cb');
602 var $cb = $(line).closest('.cb');
603 $cb.removeClass('cb-collapsed')
603 $cb.removeClass('cb-collapsed')
604 }
604 }
605 if (line.length > 0){
605 if (line.length > 0){
606 offsetScroll(line, 70);
606 offsetScroll(line, 70);
607 }
607 }
608 }
608 }
609
609
610 versionController = new VersionController();
610 versionController = new VersionController();
611 versionController.init();
611 versionController.init();
612
612
613
613
614 $(function(){
614 $(function(){
615 ReviewerAutoComplete('user');
615 ReviewerAutoComplete('user');
616 // custom code mirror
616 // custom code mirror
617 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
617 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
618
618
619 var PRDetails = {
619 var PRDetails = {
620 editButton: $('#open_edit_pullrequest'),
620 editButton: $('#open_edit_pullrequest'),
621 closeButton: $('#close_edit_pullrequest'),
621 closeButton: $('#close_edit_pullrequest'),
622 deleteButton: $('#delete_pullrequest'),
622 deleteButton: $('#delete_pullrequest'),
623 viewFields: $('#pr-desc, #pr-title'),
623 viewFields: $('#pr-desc, #pr-title'),
624 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
624 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
625
625
626 init: function() {
626 init: function() {
627 var that = this;
627 var that = this;
628 this.editButton.on('click', function(e) { that.edit(); });
628 this.editButton.on('click', function(e) { that.edit(); });
629 this.closeButton.on('click', function(e) { that.view(); });
629 this.closeButton.on('click', function(e) { that.view(); });
630 },
630 },
631
631
632 edit: function(event) {
632 edit: function(event) {
633 this.viewFields.hide();
633 this.viewFields.hide();
634 this.editButton.hide();
634 this.editButton.hide();
635 this.deleteButton.hide();
635 this.deleteButton.hide();
636 this.closeButton.show();
636 this.closeButton.show();
637 this.editFields.show();
637 this.editFields.show();
638 codeMirrorInstance.refresh();
638 codeMirrorInstance.refresh();
639 },
639 },
640
640
641 view: function(event) {
641 view: function(event) {
642 this.editButton.show();
642 this.editButton.show();
643 this.deleteButton.show();
643 this.deleteButton.show();
644 this.editFields.hide();
644 this.editFields.hide();
645 this.closeButton.hide();
645 this.closeButton.hide();
646 this.viewFields.show();
646 this.viewFields.show();
647 }
647 }
648 };
648 };
649
649
650 var ReviewersPanel = {
650 var ReviewersPanel = {
651 editButton: $('#open_edit_reviewers'),
651 editButton: $('#open_edit_reviewers'),
652 closeButton: $('#close_edit_reviewers'),
652 closeButton: $('#close_edit_reviewers'),
653 addButton: $('#add_reviewer_input'),
653 addButton: $('#add_reviewer_input'),
654 removeButtons: $('.reviewer_member_remove'),
654 removeButtons: $('.reviewer_member_remove'),
655
655
656 init: function() {
656 init: function() {
657 var that = this;
657 var that = this;
658 this.editButton.on('click', function(e) { that.edit(); });
658 this.editButton.on('click', function(e) { that.edit(); });
659 this.closeButton.on('click', function(e) { that.close(); });
659 this.closeButton.on('click', function(e) { that.close(); });
660 },
660 },
661
661
662 edit: function(event) {
662 edit: function(event) {
663 this.editButton.hide();
663 this.editButton.hide();
664 this.closeButton.show();
664 this.closeButton.show();
665 this.addButton.show();
665 this.addButton.show();
666 this.removeButtons.css('visibility', 'visible');
666 this.removeButtons.css('visibility', 'visible');
667 },
667 },
668
668
669 close: function(event) {
669 close: function(event) {
670 this.editButton.show();
670 this.editButton.show();
671 this.closeButton.hide();
671 this.closeButton.hide();
672 this.addButton.hide();
672 this.addButton.hide();
673 this.removeButtons.css('visibility', 'hidden');
673 this.removeButtons.css('visibility', 'hidden');
674 }
674 }
675 };
675 };
676
676
677 PRDetails.init();
677 PRDetails.init();
678 ReviewersPanel.init();
678 ReviewersPanel.init();
679
679
680 showOutdated = function(self){
680 showOutdated = function(self){
681 $('.comment-inline.comment-outdated').show();
681 $('.comment-inline.comment-outdated').show();
682 $('.filediff-outdated').show();
682 $('.filediff-outdated').show();
683 $('.showOutdatedComments').hide();
683 $('.showOutdatedComments').hide();
684 $('.hideOutdatedComments').show();
684 $('.hideOutdatedComments').show();
685 };
685 };
686
686
687 hideOutdated = function(self){
687 hideOutdated = function(self){
688 $('.comment-inline.comment-outdated').hide();
688 $('.comment-inline.comment-outdated').hide();
689 $('.filediff-outdated').hide();
689 $('.filediff-outdated').hide();
690 $('.hideOutdatedComments').hide();
690 $('.hideOutdatedComments').hide();
691 $('.showOutdatedComments').show();
691 $('.showOutdatedComments').show();
692 };
692 };
693
693
694 refreshMergeChecks = function(){
694 refreshMergeChecks = function(){
695 var loadUrl = "${h.url.current(merge_checks=1)}";
695 var loadUrl = "${h.url.current(merge_checks=1)}";
696 $('.pull-request-merge').css('opacity', 0.3);
696 $('.pull-request-merge').css('opacity', 0.3);
697 $('.action-buttons-extra').css('opacity', 0.3);
698
697 $('.pull-request-merge').load(
699 $('.pull-request-merge').load(
698 loadUrl,function() {
700 loadUrl, function() {
699 $('.pull-request-merge').css('opacity', 1);
701 $('.pull-request-merge').css('opacity', 1);
702
703 $('.action-buttons-extra').css('opacity', 1);
704 injectCloseAction();
700 }
705 }
701 );
706 );
702 };
707 };
703
708
709 injectCloseAction = function() {
710 var closeAction = $('#close-pull-request-action').html();
711 var $actionButtons = $('.action-buttons-extra');
712 // clear the action before
713 $actionButtons.html("");
714 $actionButtons.html(closeAction);
715 };
716
717 closePullRequest = function (status) {
718 // inject closing flag
719 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
720 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
721 $(generalCommentForm.submitForm).submit();
722 };
723
704 $('#show-outdated-comments').on('click', function(e){
724 $('#show-outdated-comments').on('click', function(e){
705 var button = $(this);
725 var button = $(this);
706 var outdated = $('.comment-outdated');
726 var outdated = $('.comment-outdated');
707
727
708 if (button.html() === "(Show)") {
728 if (button.html() === "(Show)") {
709 button.html("(Hide)");
729 button.html("(Hide)");
710 outdated.show();
730 outdated.show();
711 } else {
731 } else {
712 button.html("(Show)");
732 button.html("(Show)");
713 outdated.hide();
733 outdated.hide();
714 }
734 }
715 });
735 });
716
736
717 $('.show-inline-comments').on('change', function(e){
737 $('.show-inline-comments').on('change', function(e){
718 var show = 'none';
738 var show = 'none';
719 var target = e.currentTarget;
739 var target = e.currentTarget;
720 if(target.checked){
740 if(target.checked){
721 show = ''
741 show = ''
722 }
742 }
723 var boxid = $(target).attr('id_for');
743 var boxid = $(target).attr('id_for');
724 var comments = $('#{0} .inline-comments'.format(boxid));
744 var comments = $('#{0} .inline-comments'.format(boxid));
725 var fn_display = function(idx){
745 var fn_display = function(idx){
726 $(this).css('display', show);
746 $(this).css('display', show);
727 };
747 };
728 $(comments).each(fn_display);
748 $(comments).each(fn_display);
729 var btns = $('#{0} .inline-comments-button'.format(boxid));
749 var btns = $('#{0} .inline-comments-button'.format(boxid));
730 $(btns).each(fn_display);
750 $(btns).each(fn_display);
731 });
751 });
732
752
733 $('#merge_pull_request_form').submit(function() {
753 $('#merge_pull_request_form').submit(function() {
734 if (!$('#merge_pull_request').attr('disabled')) {
754 if (!$('#merge_pull_request').attr('disabled')) {
735 $('#merge_pull_request').attr('disabled', 'disabled');
755 $('#merge_pull_request').attr('disabled', 'disabled');
736 }
756 }
737 return true;
757 return true;
738 });
758 });
739
759
740 $('#edit_pull_request').on('click', function(e){
760 $('#edit_pull_request').on('click', function(e){
741 var title = $('#pr-title-input').val();
761 var title = $('#pr-title-input').val();
742 var description = codeMirrorInstance.getValue();
762 var description = codeMirrorInstance.getValue();
743 editPullRequest(
763 editPullRequest(
744 "${c.repo_name}", "${c.pull_request.pull_request_id}",
764 "${c.repo_name}", "${c.pull_request.pull_request_id}",
745 title, description);
765 title, description);
746 });
766 });
747
767
748 $('#update_pull_request').on('click', function(e){
768 $('#update_pull_request').on('click', function(e){
749 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
769 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
750 });
770 });
751
771
752 $('#update_commits').on('click', function(e){
772 $('#update_commits').on('click', function(e){
753 var isDisabled = !$(e.currentTarget).attr('disabled');
773 var isDisabled = !$(e.currentTarget).attr('disabled');
754 $(e.currentTarget).text(_gettext('Updating...'));
774 $(e.currentTarget).text(_gettext('Updating...'));
755 $(e.currentTarget).attr('disabled', 'disabled');
775 $(e.currentTarget).attr('disabled', 'disabled');
756 if(isDisabled){
776 if(isDisabled){
757 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
777 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
758 }
778 }
759
779
760 });
780 });
761 // fixing issue with caches on firefox
781 // fixing issue with caches on firefox
762 $('#update_commits').removeAttr("disabled");
782 $('#update_commits').removeAttr("disabled");
763
783
764 $('#close_pull_request').on('click', function(e){
784 $('#close_pull_request').on('click', function(e){
765 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
785 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
766 });
786 });
767
787
768 $('.show-inline-comments').on('click', function(e){
788 $('.show-inline-comments').on('click', function(e){
769 var boxid = $(this).attr('data-comment-id');
789 var boxid = $(this).attr('data-comment-id');
770 var button = $(this);
790 var button = $(this);
771
791
772 if(button.hasClass("comments-visible")) {
792 if(button.hasClass("comments-visible")) {
773 $('#{0} .inline-comments'.format(boxid)).each(function(index){
793 $('#{0} .inline-comments'.format(boxid)).each(function(index){
774 $(this).hide();
794 $(this).hide();
775 });
795 });
776 button.removeClass("comments-visible");
796 button.removeClass("comments-visible");
777 } else {
797 } else {
778 $('#{0} .inline-comments'.format(boxid)).each(function(index){
798 $('#{0} .inline-comments'.format(boxid)).each(function(index){
779 $(this).show();
799 $(this).show();
780 });
800 });
781 button.addClass("comments-visible");
801 button.addClass("comments-visible");
782 }
802 }
783 });
803 });
784
804
785 // register submit callback on commentForm form to track TODOs
805 // register submit callback on commentForm form to track TODOs
786 window.commentFormGlobalSubmitSuccessCallback = function(){
806 window.commentFormGlobalSubmitSuccessCallback = function(){
787 refreshMergeChecks();
807 refreshMergeChecks();
788 };
808 };
809 // initial injection
810 injectCloseAction();
789
811
790 })
812 })
791 </script>
813 </script>
792
814
793 </div>
815 </div>
794 </div>
816 </div>
795
817
796 </%def>
818 </%def>
@@ -1,1067 +1,1077 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 from webob.exc import HTTPNotFound
23 from webob.exc import HTTPNotFound
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib.vcs.nodes import FileNode
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification)
29 PullRequest, ChangesetStatus, UserLog, Notification)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.model.repo import RepoModel
33 from rhodecode.model.repo import RepoModel
34 from rhodecode.tests import assert_session_flash, url, TEST_USER_ADMIN_LOGIN
34 from rhodecode.tests import assert_session_flash, url, TEST_USER_ADMIN_LOGIN
35 from rhodecode.tests.utils import AssertResponse
35 from rhodecode.tests.utils import AssertResponse
36
36
37
37
38 @pytest.mark.usefixtures('app', 'autologin_user')
38 @pytest.mark.usefixtures('app', 'autologin_user')
39 @pytest.mark.backends("git", "hg")
39 @pytest.mark.backends("git", "hg")
40 class TestPullrequestsController:
40 class TestPullrequestsController:
41
41
42 def test_index(self, backend):
42 def test_index(self, backend):
43 self.app.get(url(
43 self.app.get(url(
44 controller='pullrequests', action='index',
44 controller='pullrequests', action='index',
45 repo_name=backend.repo_name))
45 repo_name=backend.repo_name))
46
46
47 def test_option_menu_create_pull_request_exists(self, backend):
47 def test_option_menu_create_pull_request_exists(self, backend):
48 repo_name = backend.repo_name
48 repo_name = backend.repo_name
49 response = self.app.get(url('summary_home', repo_name=repo_name))
49 response = self.app.get(url('summary_home', repo_name=repo_name))
50
50
51 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
51 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
52 'pullrequest', repo_name=repo_name)
52 'pullrequest', repo_name=repo_name)
53 response.mustcontain(create_pr_link)
53 response.mustcontain(create_pr_link)
54
54
55 def test_global_redirect_of_pr(self, backend, pr_util):
55 def test_global_redirect_of_pr(self, backend, pr_util):
56 pull_request = pr_util.create_pull_request()
56 pull_request = pr_util.create_pull_request()
57
57
58 response = self.app.get(
58 response = self.app.get(
59 url('pull_requests_global',
59 url('pull_requests_global',
60 pull_request_id=pull_request.pull_request_id))
60 pull_request_id=pull_request.pull_request_id))
61
61
62 repo_name = pull_request.target_repo.repo_name
62 repo_name = pull_request.target_repo.repo_name
63 redirect_url = url('pullrequest_show', repo_name=repo_name,
63 redirect_url = url('pullrequest_show', repo_name=repo_name,
64 pull_request_id=pull_request.pull_request_id)
64 pull_request_id=pull_request.pull_request_id)
65 assert response.status == '302 Found'
65 assert response.status == '302 Found'
66 assert redirect_url in response.location
66 assert redirect_url in response.location
67
67
68 def test_create_pr_form_with_raw_commit_id(self, backend):
68 def test_create_pr_form_with_raw_commit_id(self, backend):
69 repo = backend.repo
69 repo = backend.repo
70
70
71 self.app.get(
71 self.app.get(
72 url(controller='pullrequests', action='index',
72 url(controller='pullrequests', action='index',
73 repo_name=repo.repo_name,
73 repo_name=repo.repo_name,
74 commit=repo.get_commit().raw_id),
74 commit=repo.get_commit().raw_id),
75 status=200)
75 status=200)
76
76
77 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
77 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
78 def test_show(self, pr_util, pr_merge_enabled):
78 def test_show(self, pr_util, pr_merge_enabled):
79 pull_request = pr_util.create_pull_request(
79 pull_request = pr_util.create_pull_request(
80 mergeable=pr_merge_enabled, enable_notifications=False)
80 mergeable=pr_merge_enabled, enable_notifications=False)
81
81
82 response = self.app.get(url(
82 response = self.app.get(url(
83 controller='pullrequests', action='show',
83 controller='pullrequests', action='show',
84 repo_name=pull_request.target_repo.scm_instance().name,
84 repo_name=pull_request.target_repo.scm_instance().name,
85 pull_request_id=str(pull_request.pull_request_id)))
85 pull_request_id=str(pull_request.pull_request_id)))
86
86
87 for commit_id in pull_request.revisions:
87 for commit_id in pull_request.revisions:
88 response.mustcontain(commit_id)
88 response.mustcontain(commit_id)
89
89
90 assert pull_request.target_ref_parts.type in response
90 assert pull_request.target_ref_parts.type in response
91 assert pull_request.target_ref_parts.name in response
91 assert pull_request.target_ref_parts.name in response
92 target_clone_url = pull_request.target_repo.clone_url()
92 target_clone_url = pull_request.target_repo.clone_url()
93 assert target_clone_url in response
93 assert target_clone_url in response
94
94
95 assert 'class="pull-request-merge"' in response
95 assert 'class="pull-request-merge"' in response
96 assert (
96 assert (
97 'Server-side pull request merging is disabled.'
97 'Server-side pull request merging is disabled.'
98 in response) != pr_merge_enabled
98 in response) != pr_merge_enabled
99
99
100 def test_close_status_visibility(self, pr_util, csrf_token):
100 def test_close_status_visibility(self, pr_util, csrf_token):
101 from rhodecode.tests.functional.test_login import login_url, logut_url
101 from rhodecode.tests.functional.test_login import login_url, logut_url
102 # Logout
102 # Logout
103 response = self.app.post(
103 response = self.app.post(
104 logut_url,
104 logut_url,
105 params={'csrf_token': csrf_token})
105 params={'csrf_token': csrf_token})
106 # Login as regular user
106 # Login as regular user
107 response = self.app.post(login_url,
107 response = self.app.post(login_url,
108 {'username': 'test_regular',
108 {'username': 'test_regular',
109 'password': 'test12'})
109 'password': 'test12'})
110
110
111 pull_request = pr_util.create_pull_request(author='test_regular')
111 pull_request = pr_util.create_pull_request(author='test_regular')
112
112
113 response = self.app.get(url(
113 response = self.app.get(url(
114 controller='pullrequests', action='show',
114 controller='pullrequests', action='show',
115 repo_name=pull_request.target_repo.scm_instance().name,
115 repo_name=pull_request.target_repo.scm_instance().name,
116 pull_request_id=str(pull_request.pull_request_id)))
116 pull_request_id=str(pull_request.pull_request_id)))
117
117
118 assert 'Server-side pull request merging is disabled.' in response
118 response.mustcontain('Server-side pull request merging is disabled.')
119 assert 'value="forced_closed"' in response
119
120 assert_response = response.assert_response()
121 assert_response.one_element_exists('#close-pull-request-action')
120
122
121 def test_show_invalid_commit_id(self, pr_util):
123 def test_show_invalid_commit_id(self, pr_util):
122 # Simulating invalid revisions which will cause a lookup error
124 # Simulating invalid revisions which will cause a lookup error
123 pull_request = pr_util.create_pull_request()
125 pull_request = pr_util.create_pull_request()
124 pull_request.revisions = ['invalid']
126 pull_request.revisions = ['invalid']
125 Session().add(pull_request)
127 Session().add(pull_request)
126 Session().commit()
128 Session().commit()
127
129
128 response = self.app.get(url(
130 response = self.app.get(url(
129 controller='pullrequests', action='show',
131 controller='pullrequests', action='show',
130 repo_name=pull_request.target_repo.scm_instance().name,
132 repo_name=pull_request.target_repo.scm_instance().name,
131 pull_request_id=str(pull_request.pull_request_id)))
133 pull_request_id=str(pull_request.pull_request_id)))
132
134
133 for commit_id in pull_request.revisions:
135 for commit_id in pull_request.revisions:
134 response.mustcontain(commit_id)
136 response.mustcontain(commit_id)
135
137
136 def test_show_invalid_source_reference(self, pr_util):
138 def test_show_invalid_source_reference(self, pr_util):
137 pull_request = pr_util.create_pull_request()
139 pull_request = pr_util.create_pull_request()
138 pull_request.source_ref = 'branch:b:invalid'
140 pull_request.source_ref = 'branch:b:invalid'
139 Session().add(pull_request)
141 Session().add(pull_request)
140 Session().commit()
142 Session().commit()
141
143
142 self.app.get(url(
144 self.app.get(url(
143 controller='pullrequests', action='show',
145 controller='pullrequests', action='show',
144 repo_name=pull_request.target_repo.scm_instance().name,
146 repo_name=pull_request.target_repo.scm_instance().name,
145 pull_request_id=str(pull_request.pull_request_id)))
147 pull_request_id=str(pull_request.pull_request_id)))
146
148
147 def test_edit_title_description(self, pr_util, csrf_token):
149 def test_edit_title_description(self, pr_util, csrf_token):
148 pull_request = pr_util.create_pull_request()
150 pull_request = pr_util.create_pull_request()
149 pull_request_id = pull_request.pull_request_id
151 pull_request_id = pull_request.pull_request_id
150
152
151 response = self.app.post(
153 response = self.app.post(
152 url(controller='pullrequests', action='update',
154 url(controller='pullrequests', action='update',
153 repo_name=pull_request.target_repo.repo_name,
155 repo_name=pull_request.target_repo.repo_name,
154 pull_request_id=str(pull_request_id)),
156 pull_request_id=str(pull_request_id)),
155 params={
157 params={
156 'edit_pull_request': 'true',
158 'edit_pull_request': 'true',
157 '_method': 'put',
159 '_method': 'put',
158 'title': 'New title',
160 'title': 'New title',
159 'description': 'New description',
161 'description': 'New description',
160 'csrf_token': csrf_token})
162 'csrf_token': csrf_token})
161
163
162 assert_session_flash(
164 assert_session_flash(
163 response, u'Pull request title & description updated.',
165 response, u'Pull request title & description updated.',
164 category='success')
166 category='success')
165
167
166 pull_request = PullRequest.get(pull_request_id)
168 pull_request = PullRequest.get(pull_request_id)
167 assert pull_request.title == 'New title'
169 assert pull_request.title == 'New title'
168 assert pull_request.description == 'New description'
170 assert pull_request.description == 'New description'
169
171
170 def test_edit_title_description_closed(self, pr_util, csrf_token):
172 def test_edit_title_description_closed(self, pr_util, csrf_token):
171 pull_request = pr_util.create_pull_request()
173 pull_request = pr_util.create_pull_request()
172 pull_request_id = pull_request.pull_request_id
174 pull_request_id = pull_request.pull_request_id
173 pr_util.close()
175 pr_util.close()
174
176
175 response = self.app.post(
177 response = self.app.post(
176 url(controller='pullrequests', action='update',
178 url(controller='pullrequests', action='update',
177 repo_name=pull_request.target_repo.repo_name,
179 repo_name=pull_request.target_repo.repo_name,
178 pull_request_id=str(pull_request_id)),
180 pull_request_id=str(pull_request_id)),
179 params={
181 params={
180 'edit_pull_request': 'true',
182 'edit_pull_request': 'true',
181 '_method': 'put',
183 '_method': 'put',
182 'title': 'New title',
184 'title': 'New title',
183 'description': 'New description',
185 'description': 'New description',
184 'csrf_token': csrf_token})
186 'csrf_token': csrf_token})
185
187
186 assert_session_flash(
188 assert_session_flash(
187 response, u'Cannot update closed pull requests.',
189 response, u'Cannot update closed pull requests.',
188 category='error')
190 category='error')
189
191
190 def test_update_invalid_source_reference(self, pr_util, csrf_token):
192 def test_update_invalid_source_reference(self, pr_util, csrf_token):
191 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
193 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
192
194
193 pull_request = pr_util.create_pull_request()
195 pull_request = pr_util.create_pull_request()
194 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
196 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
195 Session().add(pull_request)
197 Session().add(pull_request)
196 Session().commit()
198 Session().commit()
197
199
198 pull_request_id = pull_request.pull_request_id
200 pull_request_id = pull_request.pull_request_id
199
201
200 response = self.app.post(
202 response = self.app.post(
201 url(controller='pullrequests', action='update',
203 url(controller='pullrequests', action='update',
202 repo_name=pull_request.target_repo.repo_name,
204 repo_name=pull_request.target_repo.repo_name,
203 pull_request_id=str(pull_request_id)),
205 pull_request_id=str(pull_request_id)),
204 params={'update_commits': 'true', '_method': 'put',
206 params={'update_commits': 'true', '_method': 'put',
205 'csrf_token': csrf_token})
207 'csrf_token': csrf_token})
206
208
207 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
209 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
208 UpdateFailureReason.MISSING_SOURCE_REF]
210 UpdateFailureReason.MISSING_SOURCE_REF]
209 assert_session_flash(response, expected_msg, category='error')
211 assert_session_flash(response, expected_msg, category='error')
210
212
211 def test_missing_target_reference(self, pr_util, csrf_token):
213 def test_missing_target_reference(self, pr_util, csrf_token):
212 from rhodecode.lib.vcs.backends.base import MergeFailureReason
214 from rhodecode.lib.vcs.backends.base import MergeFailureReason
213 pull_request = pr_util.create_pull_request(
215 pull_request = pr_util.create_pull_request(
214 approved=True, mergeable=True)
216 approved=True, mergeable=True)
215 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
217 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
216 Session().add(pull_request)
218 Session().add(pull_request)
217 Session().commit()
219 Session().commit()
218
220
219 pull_request_id = pull_request.pull_request_id
221 pull_request_id = pull_request.pull_request_id
220 pull_request_url = url(
222 pull_request_url = url(
221 controller='pullrequests', action='show',
223 controller='pullrequests', action='show',
222 repo_name=pull_request.target_repo.repo_name,
224 repo_name=pull_request.target_repo.repo_name,
223 pull_request_id=str(pull_request_id))
225 pull_request_id=str(pull_request_id))
224
226
225 response = self.app.get(pull_request_url)
227 response = self.app.get(pull_request_url)
226
228
227 assertr = AssertResponse(response)
229 assertr = AssertResponse(response)
228 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
230 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
229 MergeFailureReason.MISSING_TARGET_REF]
231 MergeFailureReason.MISSING_TARGET_REF]
230 assertr.element_contains(
232 assertr.element_contains(
231 'span[data-role="merge-message"]', str(expected_msg))
233 'span[data-role="merge-message"]', str(expected_msg))
232
234
233 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
235 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
234 pull_request = pr_util.create_pull_request(approved=True)
236 pull_request = pr_util.create_pull_request(approved=True)
235 pull_request_id = pull_request.pull_request_id
237 pull_request_id = pull_request.pull_request_id
236 author = pull_request.user_id
238 author = pull_request.user_id
237 repo = pull_request.target_repo.repo_id
239 repo = pull_request.target_repo.repo_id
238
240
239 self.app.post(
241 self.app.post(
240 url(controller='pullrequests',
242 url(controller='pullrequests',
241 action='comment',
243 action='comment',
242 repo_name=pull_request.target_repo.scm_instance().name,
244 repo_name=pull_request.target_repo.scm_instance().name,
243 pull_request_id=str(pull_request_id)),
245 pull_request_id=str(pull_request_id)),
244 params={
246 params={
245 'changeset_status':
247 'changeset_status': ChangesetStatus.STATUS_APPROVED,
246 ChangesetStatus.STATUS_APPROVED + '_closed',
248 'close_pull_request': '1',
247 'change_changeset_status': 'on',
249 'text': 'Closing a PR',
248 'text': '',
249 'csrf_token': csrf_token},
250 'csrf_token': csrf_token},
250 status=302)
251 status=302)
251
252
252 action = 'user_closed_pull_request:%d' % pull_request_id
253 action = 'user_closed_pull_request:%d' % pull_request_id
253 journal = UserLog.query()\
254 journal = UserLog.query()\
254 .filter(UserLog.user_id == author)\
255 .filter(UserLog.user_id == author)\
255 .filter(UserLog.repository_id == repo)\
256 .filter(UserLog.repository_id == repo)\
256 .filter(UserLog.action == action)\
257 .filter(UserLog.action == action)\
257 .all()
258 .all()
258 assert len(journal) == 1
259 assert len(journal) == 1
259
260
261 pull_request = PullRequest.get(pull_request_id)
262 assert pull_request.is_closed()
263
264 # check only the latest status, not the review status
265 status = ChangesetStatusModel().get_status(
266 pull_request.source_repo, pull_request=pull_request)
267 assert status == ChangesetStatus.STATUS_APPROVED
268
260 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
269 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
261 pull_request = pr_util.create_pull_request()
270 pull_request = pr_util.create_pull_request()
262 pull_request_id = pull_request.pull_request_id
271 pull_request_id = pull_request.pull_request_id
263 response = self.app.post(
272 response = self.app.post(
264 url(controller='pullrequests',
273 url(controller='pullrequests',
265 action='update',
274 action='update',
266 repo_name=pull_request.target_repo.scm_instance().name,
275 repo_name=pull_request.target_repo.scm_instance().name,
267 pull_request_id=str(pull_request.pull_request_id)),
276 pull_request_id=str(pull_request.pull_request_id)),
268 params={'close_pull_request': 'true', '_method': 'put',
277 params={'close_pull_request': 'true', '_method': 'put',
269 'csrf_token': csrf_token})
278 'csrf_token': csrf_token})
270
279
271 pull_request = PullRequest.get(pull_request_id)
280 pull_request = PullRequest.get(pull_request_id)
272
281
273 assert response.json is True
282 assert response.json is True
274 assert pull_request.is_closed()
283 assert pull_request.is_closed()
275
284
276 # check only the latest status, not the review status
285 # check only the latest status, not the review status
277 status = ChangesetStatusModel().get_status(
286 status = ChangesetStatusModel().get_status(
278 pull_request.source_repo, pull_request=pull_request)
287 pull_request.source_repo, pull_request=pull_request)
279 assert status == ChangesetStatus.STATUS_REJECTED
288 assert status == ChangesetStatus.STATUS_REJECTED
280
289
281 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
290 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
282 pull_request = pr_util.create_pull_request()
291 pull_request = pr_util.create_pull_request()
283 pull_request_id = pull_request.pull_request_id
292 pull_request_id = pull_request.pull_request_id
284 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
293 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
285 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
294 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
286 author = pull_request.user_id
295 author = pull_request.user_id
287 repo = pull_request.target_repo.repo_id
296 repo = pull_request.target_repo.repo_id
288 self.app.post(
297 self.app.post(
289 url(controller='pullrequests',
298 url(controller='pullrequests',
290 action='comment',
299 action='comment',
291 repo_name=pull_request.target_repo.scm_instance().name,
300 repo_name=pull_request.target_repo.scm_instance().name,
292 pull_request_id=str(pull_request_id)),
301 pull_request_id=str(pull_request_id)),
293 params={
302 params={
294 'changeset_status': 'forced_closed',
303 'changeset_status': 'rejected',
304 'close_pull_request': '1',
295 'csrf_token': csrf_token},
305 'csrf_token': csrf_token},
296 status=302)
306 status=302)
297
307
298 pull_request = PullRequest.get(pull_request_id)
308 pull_request = PullRequest.get(pull_request_id)
299
309
300 action = 'user_closed_pull_request:%d' % pull_request_id
310 action = 'user_closed_pull_request:%d' % pull_request_id
301 journal = UserLog.query().filter(
311 journal = UserLog.query().filter(
302 UserLog.user_id == author,
312 UserLog.user_id == author,
303 UserLog.repository_id == repo,
313 UserLog.repository_id == repo,
304 UserLog.action == action).all()
314 UserLog.action == action).all()
305 assert len(journal) == 1
315 assert len(journal) == 1
306
316
307 # check only the latest status, not the review status
317 # check only the latest status, not the review status
308 status = ChangesetStatusModel().get_status(
318 status = ChangesetStatusModel().get_status(
309 pull_request.source_repo, pull_request=pull_request)
319 pull_request.source_repo, pull_request=pull_request)
310 assert status == ChangesetStatus.STATUS_REJECTED
320 assert status == ChangesetStatus.STATUS_REJECTED
311
321
312 def test_create_pull_request(self, backend, csrf_token):
322 def test_create_pull_request(self, backend, csrf_token):
313 commits = [
323 commits = [
314 {'message': 'ancestor'},
324 {'message': 'ancestor'},
315 {'message': 'change'},
325 {'message': 'change'},
316 {'message': 'change2'},
326 {'message': 'change2'},
317 ]
327 ]
318 commit_ids = backend.create_master_repo(commits)
328 commit_ids = backend.create_master_repo(commits)
319 target = backend.create_repo(heads=['ancestor'])
329 target = backend.create_repo(heads=['ancestor'])
320 source = backend.create_repo(heads=['change2'])
330 source = backend.create_repo(heads=['change2'])
321
331
322 response = self.app.post(
332 response = self.app.post(
323 url(
333 url(
324 controller='pullrequests',
334 controller='pullrequests',
325 action='create',
335 action='create',
326 repo_name=source.repo_name
336 repo_name=source.repo_name
327 ),
337 ),
328 [
338 [
329 ('source_repo', source.repo_name),
339 ('source_repo', source.repo_name),
330 ('source_ref', 'branch:default:' + commit_ids['change2']),
340 ('source_ref', 'branch:default:' + commit_ids['change2']),
331 ('target_repo', target.repo_name),
341 ('target_repo', target.repo_name),
332 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
342 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
333 ('pullrequest_desc', 'Description'),
343 ('pullrequest_desc', 'Description'),
334 ('pullrequest_title', 'Title'),
344 ('pullrequest_title', 'Title'),
335 ('__start__', 'review_members:sequence'),
345 ('__start__', 'review_members:sequence'),
336 ('__start__', 'reviewer:mapping'),
346 ('__start__', 'reviewer:mapping'),
337 ('user_id', '1'),
347 ('user_id', '1'),
338 ('__start__', 'reasons:sequence'),
348 ('__start__', 'reasons:sequence'),
339 ('reason', 'Some reason'),
349 ('reason', 'Some reason'),
340 ('__end__', 'reasons:sequence'),
350 ('__end__', 'reasons:sequence'),
341 ('__end__', 'reviewer:mapping'),
351 ('__end__', 'reviewer:mapping'),
342 ('__end__', 'review_members:sequence'),
352 ('__end__', 'review_members:sequence'),
343 ('__start__', 'revisions:sequence'),
353 ('__start__', 'revisions:sequence'),
344 ('revisions', commit_ids['change']),
354 ('revisions', commit_ids['change']),
345 ('revisions', commit_ids['change2']),
355 ('revisions', commit_ids['change2']),
346 ('__end__', 'revisions:sequence'),
356 ('__end__', 'revisions:sequence'),
347 ('user', ''),
357 ('user', ''),
348 ('csrf_token', csrf_token),
358 ('csrf_token', csrf_token),
349 ],
359 ],
350 status=302)
360 status=302)
351
361
352 location = response.headers['Location']
362 location = response.headers['Location']
353 pull_request_id = int(location.rsplit('/', 1)[1])
363 pull_request_id = int(location.rsplit('/', 1)[1])
354 pull_request = PullRequest.get(pull_request_id)
364 pull_request = PullRequest.get(pull_request_id)
355
365
356 # check that we have now both revisions
366 # check that we have now both revisions
357 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
367 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
358 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
368 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
359 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
369 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
360 assert pull_request.target_ref == expected_target_ref
370 assert pull_request.target_ref == expected_target_ref
361
371
362 def test_reviewer_notifications(self, backend, csrf_token):
372 def test_reviewer_notifications(self, backend, csrf_token):
363 # We have to use the app.post for this test so it will create the
373 # We have to use the app.post for this test so it will create the
364 # notifications properly with the new PR
374 # notifications properly with the new PR
365 commits = [
375 commits = [
366 {'message': 'ancestor',
376 {'message': 'ancestor',
367 'added': [FileNode('file_A', content='content_of_ancestor')]},
377 'added': [FileNode('file_A', content='content_of_ancestor')]},
368 {'message': 'change',
378 {'message': 'change',
369 'added': [FileNode('file_a', content='content_of_change')]},
379 'added': [FileNode('file_a', content='content_of_change')]},
370 {'message': 'change-child'},
380 {'message': 'change-child'},
371 {'message': 'ancestor-child', 'parents': ['ancestor'],
381 {'message': 'ancestor-child', 'parents': ['ancestor'],
372 'added': [
382 'added': [
373 FileNode('file_B', content='content_of_ancestor_child')]},
383 FileNode('file_B', content='content_of_ancestor_child')]},
374 {'message': 'ancestor-child-2'},
384 {'message': 'ancestor-child-2'},
375 ]
385 ]
376 commit_ids = backend.create_master_repo(commits)
386 commit_ids = backend.create_master_repo(commits)
377 target = backend.create_repo(heads=['ancestor-child'])
387 target = backend.create_repo(heads=['ancestor-child'])
378 source = backend.create_repo(heads=['change'])
388 source = backend.create_repo(heads=['change'])
379
389
380 response = self.app.post(
390 response = self.app.post(
381 url(
391 url(
382 controller='pullrequests',
392 controller='pullrequests',
383 action='create',
393 action='create',
384 repo_name=source.repo_name
394 repo_name=source.repo_name
385 ),
395 ),
386 [
396 [
387 ('source_repo', source.repo_name),
397 ('source_repo', source.repo_name),
388 ('source_ref', 'branch:default:' + commit_ids['change']),
398 ('source_ref', 'branch:default:' + commit_ids['change']),
389 ('target_repo', target.repo_name),
399 ('target_repo', target.repo_name),
390 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
400 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
391 ('pullrequest_desc', 'Description'),
401 ('pullrequest_desc', 'Description'),
392 ('pullrequest_title', 'Title'),
402 ('pullrequest_title', 'Title'),
393 ('__start__', 'review_members:sequence'),
403 ('__start__', 'review_members:sequence'),
394 ('__start__', 'reviewer:mapping'),
404 ('__start__', 'reviewer:mapping'),
395 ('user_id', '2'),
405 ('user_id', '2'),
396 ('__start__', 'reasons:sequence'),
406 ('__start__', 'reasons:sequence'),
397 ('reason', 'Some reason'),
407 ('reason', 'Some reason'),
398 ('__end__', 'reasons:sequence'),
408 ('__end__', 'reasons:sequence'),
399 ('__end__', 'reviewer:mapping'),
409 ('__end__', 'reviewer:mapping'),
400 ('__end__', 'review_members:sequence'),
410 ('__end__', 'review_members:sequence'),
401 ('__start__', 'revisions:sequence'),
411 ('__start__', 'revisions:sequence'),
402 ('revisions', commit_ids['change']),
412 ('revisions', commit_ids['change']),
403 ('__end__', 'revisions:sequence'),
413 ('__end__', 'revisions:sequence'),
404 ('user', ''),
414 ('user', ''),
405 ('csrf_token', csrf_token),
415 ('csrf_token', csrf_token),
406 ],
416 ],
407 status=302)
417 status=302)
408
418
409 location = response.headers['Location']
419 location = response.headers['Location']
410 pull_request_id = int(location.rsplit('/', 1)[1])
420 pull_request_id = int(location.rsplit('/', 1)[1])
411 pull_request = PullRequest.get(pull_request_id)
421 pull_request = PullRequest.get(pull_request_id)
412
422
413 # Check that a notification was made
423 # Check that a notification was made
414 notifications = Notification.query()\
424 notifications = Notification.query()\
415 .filter(Notification.created_by == pull_request.author.user_id,
425 .filter(Notification.created_by == pull_request.author.user_id,
416 Notification.type_ == Notification.TYPE_PULL_REQUEST,
426 Notification.type_ == Notification.TYPE_PULL_REQUEST,
417 Notification.subject.contains("wants you to review "
427 Notification.subject.contains("wants you to review "
418 "pull request #%d"
428 "pull request #%d"
419 % pull_request_id))
429 % pull_request_id))
420 assert len(notifications.all()) == 1
430 assert len(notifications.all()) == 1
421
431
422 # Change reviewers and check that a notification was made
432 # Change reviewers and check that a notification was made
423 PullRequestModel().update_reviewers(
433 PullRequestModel().update_reviewers(
424 pull_request.pull_request_id, [(1, [])])
434 pull_request.pull_request_id, [(1, [])])
425 assert len(notifications.all()) == 2
435 assert len(notifications.all()) == 2
426
436
427 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
437 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
428 csrf_token):
438 csrf_token):
429 commits = [
439 commits = [
430 {'message': 'ancestor',
440 {'message': 'ancestor',
431 'added': [FileNode('file_A', content='content_of_ancestor')]},
441 'added': [FileNode('file_A', content='content_of_ancestor')]},
432 {'message': 'change',
442 {'message': 'change',
433 'added': [FileNode('file_a', content='content_of_change')]},
443 'added': [FileNode('file_a', content='content_of_change')]},
434 {'message': 'change-child'},
444 {'message': 'change-child'},
435 {'message': 'ancestor-child', 'parents': ['ancestor'],
445 {'message': 'ancestor-child', 'parents': ['ancestor'],
436 'added': [
446 'added': [
437 FileNode('file_B', content='content_of_ancestor_child')]},
447 FileNode('file_B', content='content_of_ancestor_child')]},
438 {'message': 'ancestor-child-2'},
448 {'message': 'ancestor-child-2'},
439 ]
449 ]
440 commit_ids = backend.create_master_repo(commits)
450 commit_ids = backend.create_master_repo(commits)
441 target = backend.create_repo(heads=['ancestor-child'])
451 target = backend.create_repo(heads=['ancestor-child'])
442 source = backend.create_repo(heads=['change'])
452 source = backend.create_repo(heads=['change'])
443
453
444 response = self.app.post(
454 response = self.app.post(
445 url(
455 url(
446 controller='pullrequests',
456 controller='pullrequests',
447 action='create',
457 action='create',
448 repo_name=source.repo_name
458 repo_name=source.repo_name
449 ),
459 ),
450 [
460 [
451 ('source_repo', source.repo_name),
461 ('source_repo', source.repo_name),
452 ('source_ref', 'branch:default:' + commit_ids['change']),
462 ('source_ref', 'branch:default:' + commit_ids['change']),
453 ('target_repo', target.repo_name),
463 ('target_repo', target.repo_name),
454 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
464 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
455 ('pullrequest_desc', 'Description'),
465 ('pullrequest_desc', 'Description'),
456 ('pullrequest_title', 'Title'),
466 ('pullrequest_title', 'Title'),
457 ('__start__', 'review_members:sequence'),
467 ('__start__', 'review_members:sequence'),
458 ('__start__', 'reviewer:mapping'),
468 ('__start__', 'reviewer:mapping'),
459 ('user_id', '1'),
469 ('user_id', '1'),
460 ('__start__', 'reasons:sequence'),
470 ('__start__', 'reasons:sequence'),
461 ('reason', 'Some reason'),
471 ('reason', 'Some reason'),
462 ('__end__', 'reasons:sequence'),
472 ('__end__', 'reasons:sequence'),
463 ('__end__', 'reviewer:mapping'),
473 ('__end__', 'reviewer:mapping'),
464 ('__end__', 'review_members:sequence'),
474 ('__end__', 'review_members:sequence'),
465 ('__start__', 'revisions:sequence'),
475 ('__start__', 'revisions:sequence'),
466 ('revisions', commit_ids['change']),
476 ('revisions', commit_ids['change']),
467 ('__end__', 'revisions:sequence'),
477 ('__end__', 'revisions:sequence'),
468 ('user', ''),
478 ('user', ''),
469 ('csrf_token', csrf_token),
479 ('csrf_token', csrf_token),
470 ],
480 ],
471 status=302)
481 status=302)
472
482
473 location = response.headers['Location']
483 location = response.headers['Location']
474 pull_request_id = int(location.rsplit('/', 1)[1])
484 pull_request_id = int(location.rsplit('/', 1)[1])
475 pull_request = PullRequest.get(pull_request_id)
485 pull_request = PullRequest.get(pull_request_id)
476
486
477 # target_ref has to point to the ancestor's commit_id in order to
487 # target_ref has to point to the ancestor's commit_id in order to
478 # show the correct diff
488 # show the correct diff
479 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
489 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
480 assert pull_request.target_ref == expected_target_ref
490 assert pull_request.target_ref == expected_target_ref
481
491
482 # Check generated diff contents
492 # Check generated diff contents
483 response = response.follow()
493 response = response.follow()
484 assert 'content_of_ancestor' not in response.body
494 assert 'content_of_ancestor' not in response.body
485 assert 'content_of_ancestor-child' not in response.body
495 assert 'content_of_ancestor-child' not in response.body
486 assert 'content_of_change' in response.body
496 assert 'content_of_change' in response.body
487
497
488 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
498 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
489 # Clear any previous calls to rcextensions
499 # Clear any previous calls to rcextensions
490 rhodecode.EXTENSIONS.calls.clear()
500 rhodecode.EXTENSIONS.calls.clear()
491
501
492 pull_request = pr_util.create_pull_request(
502 pull_request = pr_util.create_pull_request(
493 approved=True, mergeable=True)
503 approved=True, mergeable=True)
494 pull_request_id = pull_request.pull_request_id
504 pull_request_id = pull_request.pull_request_id
495 repo_name = pull_request.target_repo.scm_instance().name,
505 repo_name = pull_request.target_repo.scm_instance().name,
496
506
497 response = self.app.post(
507 response = self.app.post(
498 url(controller='pullrequests',
508 url(controller='pullrequests',
499 action='merge',
509 action='merge',
500 repo_name=str(repo_name[0]),
510 repo_name=str(repo_name[0]),
501 pull_request_id=str(pull_request_id)),
511 pull_request_id=str(pull_request_id)),
502 params={'csrf_token': csrf_token}).follow()
512 params={'csrf_token': csrf_token}).follow()
503
513
504 pull_request = PullRequest.get(pull_request_id)
514 pull_request = PullRequest.get(pull_request_id)
505
515
506 assert response.status_int == 200
516 assert response.status_int == 200
507 assert pull_request.is_closed()
517 assert pull_request.is_closed()
508 assert_pull_request_status(
518 assert_pull_request_status(
509 pull_request, ChangesetStatus.STATUS_APPROVED)
519 pull_request, ChangesetStatus.STATUS_APPROVED)
510
520
511 # Check the relevant log entries were added
521 # Check the relevant log entries were added
512 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
522 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
513 actions = [log.action for log in user_logs]
523 actions = [log.action for log in user_logs]
514 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
524 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
515 expected_actions = [
525 expected_actions = [
516 u'user_closed_pull_request:%d' % pull_request_id,
526 u'user_closed_pull_request:%d' % pull_request_id,
517 u'user_merged_pull_request:%d' % pull_request_id,
527 u'user_merged_pull_request:%d' % pull_request_id,
518 # The action below reflect that the post push actions were executed
528 # The action below reflect that the post push actions were executed
519 u'user_commented_pull_request:%d' % pull_request_id,
529 u'user_commented_pull_request:%d' % pull_request_id,
520 u'push:%s' % ','.join(pr_commit_ids),
530 u'push:%s' % ','.join(pr_commit_ids),
521 ]
531 ]
522 assert actions == expected_actions
532 assert actions == expected_actions
523
533
524 # Check post_push rcextension was really executed
534 # Check post_push rcextension was really executed
525 push_calls = rhodecode.EXTENSIONS.calls['post_push']
535 push_calls = rhodecode.EXTENSIONS.calls['post_push']
526 assert len(push_calls) == 1
536 assert len(push_calls) == 1
527 unused_last_call_args, last_call_kwargs = push_calls[0]
537 unused_last_call_args, last_call_kwargs = push_calls[0]
528 assert last_call_kwargs['action'] == 'push'
538 assert last_call_kwargs['action'] == 'push'
529 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
539 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
530
540
531 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
541 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
532 pull_request = pr_util.create_pull_request(mergeable=False)
542 pull_request = pr_util.create_pull_request(mergeable=False)
533 pull_request_id = pull_request.pull_request_id
543 pull_request_id = pull_request.pull_request_id
534 pull_request = PullRequest.get(pull_request_id)
544 pull_request = PullRequest.get(pull_request_id)
535
545
536 response = self.app.post(
546 response = self.app.post(
537 url(controller='pullrequests',
547 url(controller='pullrequests',
538 action='merge',
548 action='merge',
539 repo_name=pull_request.target_repo.scm_instance().name,
549 repo_name=pull_request.target_repo.scm_instance().name,
540 pull_request_id=str(pull_request.pull_request_id)),
550 pull_request_id=str(pull_request.pull_request_id)),
541 params={'csrf_token': csrf_token}).follow()
551 params={'csrf_token': csrf_token}).follow()
542
552
543 assert response.status_int == 200
553 assert response.status_int == 200
544 response.mustcontain(
554 response.mustcontain(
545 'Merge is not currently possible because of below failed checks.')
555 'Merge is not currently possible because of below failed checks.')
546 response.mustcontain('Server-side pull request merging is disabled.')
556 response.mustcontain('Server-side pull request merging is disabled.')
547
557
548 @pytest.mark.skip_backends('svn')
558 @pytest.mark.skip_backends('svn')
549 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
559 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
550 pull_request = pr_util.create_pull_request(mergeable=True)
560 pull_request = pr_util.create_pull_request(mergeable=True)
551 pull_request_id = pull_request.pull_request_id
561 pull_request_id = pull_request.pull_request_id
552 repo_name = pull_request.target_repo.scm_instance().name,
562 repo_name = pull_request.target_repo.scm_instance().name,
553
563
554 response = self.app.post(
564 response = self.app.post(
555 url(controller='pullrequests',
565 url(controller='pullrequests',
556 action='merge',
566 action='merge',
557 repo_name=str(repo_name[0]),
567 repo_name=str(repo_name[0]),
558 pull_request_id=str(pull_request_id)),
568 pull_request_id=str(pull_request_id)),
559 params={'csrf_token': csrf_token}).follow()
569 params={'csrf_token': csrf_token}).follow()
560
570
561 assert response.status_int == 200
571 assert response.status_int == 200
562
572
563 response.mustcontain(
573 response.mustcontain(
564 'Merge is not currently possible because of below failed checks.')
574 'Merge is not currently possible because of below failed checks.')
565 response.mustcontain('Pull request reviewer approval is pending.')
575 response.mustcontain('Pull request reviewer approval is pending.')
566
576
567 def test_update_source_revision(self, backend, csrf_token):
577 def test_update_source_revision(self, backend, csrf_token):
568 commits = [
578 commits = [
569 {'message': 'ancestor'},
579 {'message': 'ancestor'},
570 {'message': 'change'},
580 {'message': 'change'},
571 {'message': 'change-2'},
581 {'message': 'change-2'},
572 ]
582 ]
573 commit_ids = backend.create_master_repo(commits)
583 commit_ids = backend.create_master_repo(commits)
574 target = backend.create_repo(heads=['ancestor'])
584 target = backend.create_repo(heads=['ancestor'])
575 source = backend.create_repo(heads=['change'])
585 source = backend.create_repo(heads=['change'])
576
586
577 # create pr from a in source to A in target
587 # create pr from a in source to A in target
578 pull_request = PullRequest()
588 pull_request = PullRequest()
579 pull_request.source_repo = source
589 pull_request.source_repo = source
580 # TODO: johbo: Make sure that we write the source ref this way!
590 # TODO: johbo: Make sure that we write the source ref this way!
581 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
591 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
582 branch=backend.default_branch_name, commit_id=commit_ids['change'])
592 branch=backend.default_branch_name, commit_id=commit_ids['change'])
583 pull_request.target_repo = target
593 pull_request.target_repo = target
584
594
585 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
595 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
586 branch=backend.default_branch_name,
596 branch=backend.default_branch_name,
587 commit_id=commit_ids['ancestor'])
597 commit_id=commit_ids['ancestor'])
588 pull_request.revisions = [commit_ids['change']]
598 pull_request.revisions = [commit_ids['change']]
589 pull_request.title = u"Test"
599 pull_request.title = u"Test"
590 pull_request.description = u"Description"
600 pull_request.description = u"Description"
591 pull_request.author = UserModel().get_by_username(
601 pull_request.author = UserModel().get_by_username(
592 TEST_USER_ADMIN_LOGIN)
602 TEST_USER_ADMIN_LOGIN)
593 Session().add(pull_request)
603 Session().add(pull_request)
594 Session().commit()
604 Session().commit()
595 pull_request_id = pull_request.pull_request_id
605 pull_request_id = pull_request.pull_request_id
596
606
597 # source has ancestor - change - change-2
607 # source has ancestor - change - change-2
598 backend.pull_heads(source, heads=['change-2'])
608 backend.pull_heads(source, heads=['change-2'])
599
609
600 # update PR
610 # update PR
601 self.app.post(
611 self.app.post(
602 url(controller='pullrequests', action='update',
612 url(controller='pullrequests', action='update',
603 repo_name=target.repo_name,
613 repo_name=target.repo_name,
604 pull_request_id=str(pull_request_id)),
614 pull_request_id=str(pull_request_id)),
605 params={'update_commits': 'true', '_method': 'put',
615 params={'update_commits': 'true', '_method': 'put',
606 'csrf_token': csrf_token})
616 'csrf_token': csrf_token})
607
617
608 # check that we have now both revisions
618 # check that we have now both revisions
609 pull_request = PullRequest.get(pull_request_id)
619 pull_request = PullRequest.get(pull_request_id)
610 assert pull_request.revisions == [
620 assert pull_request.revisions == [
611 commit_ids['change-2'], commit_ids['change']]
621 commit_ids['change-2'], commit_ids['change']]
612
622
613 # TODO: johbo: this should be a test on its own
623 # TODO: johbo: this should be a test on its own
614 response = self.app.get(url(
624 response = self.app.get(url(
615 controller='pullrequests', action='index',
625 controller='pullrequests', action='index',
616 repo_name=target.repo_name))
626 repo_name=target.repo_name))
617 assert response.status_int == 200
627 assert response.status_int == 200
618 assert 'Pull request updated to' in response.body
628 assert 'Pull request updated to' in response.body
619 assert 'with 1 added, 0 removed commits.' in response.body
629 assert 'with 1 added, 0 removed commits.' in response.body
620
630
621 def test_update_target_revision(self, backend, csrf_token):
631 def test_update_target_revision(self, backend, csrf_token):
622 commits = [
632 commits = [
623 {'message': 'ancestor'},
633 {'message': 'ancestor'},
624 {'message': 'change'},
634 {'message': 'change'},
625 {'message': 'ancestor-new', 'parents': ['ancestor']},
635 {'message': 'ancestor-new', 'parents': ['ancestor']},
626 {'message': 'change-rebased'},
636 {'message': 'change-rebased'},
627 ]
637 ]
628 commit_ids = backend.create_master_repo(commits)
638 commit_ids = backend.create_master_repo(commits)
629 target = backend.create_repo(heads=['ancestor'])
639 target = backend.create_repo(heads=['ancestor'])
630 source = backend.create_repo(heads=['change'])
640 source = backend.create_repo(heads=['change'])
631
641
632 # create pr from a in source to A in target
642 # create pr from a in source to A in target
633 pull_request = PullRequest()
643 pull_request = PullRequest()
634 pull_request.source_repo = source
644 pull_request.source_repo = source
635 # TODO: johbo: Make sure that we write the source ref this way!
645 # TODO: johbo: Make sure that we write the source ref this way!
636 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
646 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
637 branch=backend.default_branch_name, commit_id=commit_ids['change'])
647 branch=backend.default_branch_name, commit_id=commit_ids['change'])
638 pull_request.target_repo = target
648 pull_request.target_repo = target
639 # TODO: johbo: Target ref should be branch based, since tip can jump
649 # TODO: johbo: Target ref should be branch based, since tip can jump
640 # from branch to branch
650 # from branch to branch
641 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
651 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
642 branch=backend.default_branch_name,
652 branch=backend.default_branch_name,
643 commit_id=commit_ids['ancestor'])
653 commit_id=commit_ids['ancestor'])
644 pull_request.revisions = [commit_ids['change']]
654 pull_request.revisions = [commit_ids['change']]
645 pull_request.title = u"Test"
655 pull_request.title = u"Test"
646 pull_request.description = u"Description"
656 pull_request.description = u"Description"
647 pull_request.author = UserModel().get_by_username(
657 pull_request.author = UserModel().get_by_username(
648 TEST_USER_ADMIN_LOGIN)
658 TEST_USER_ADMIN_LOGIN)
649 Session().add(pull_request)
659 Session().add(pull_request)
650 Session().commit()
660 Session().commit()
651 pull_request_id = pull_request.pull_request_id
661 pull_request_id = pull_request.pull_request_id
652
662
653 # target has ancestor - ancestor-new
663 # target has ancestor - ancestor-new
654 # source has ancestor - ancestor-new - change-rebased
664 # source has ancestor - ancestor-new - change-rebased
655 backend.pull_heads(target, heads=['ancestor-new'])
665 backend.pull_heads(target, heads=['ancestor-new'])
656 backend.pull_heads(source, heads=['change-rebased'])
666 backend.pull_heads(source, heads=['change-rebased'])
657
667
658 # update PR
668 # update PR
659 self.app.post(
669 self.app.post(
660 url(controller='pullrequests', action='update',
670 url(controller='pullrequests', action='update',
661 repo_name=target.repo_name,
671 repo_name=target.repo_name,
662 pull_request_id=str(pull_request_id)),
672 pull_request_id=str(pull_request_id)),
663 params={'update_commits': 'true', '_method': 'put',
673 params={'update_commits': 'true', '_method': 'put',
664 'csrf_token': csrf_token},
674 'csrf_token': csrf_token},
665 status=200)
675 status=200)
666
676
667 # check that we have now both revisions
677 # check that we have now both revisions
668 pull_request = PullRequest.get(pull_request_id)
678 pull_request = PullRequest.get(pull_request_id)
669 assert pull_request.revisions == [commit_ids['change-rebased']]
679 assert pull_request.revisions == [commit_ids['change-rebased']]
670 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
680 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
671 branch=backend.default_branch_name,
681 branch=backend.default_branch_name,
672 commit_id=commit_ids['ancestor-new'])
682 commit_id=commit_ids['ancestor-new'])
673
683
674 # TODO: johbo: This should be a test on its own
684 # TODO: johbo: This should be a test on its own
675 response = self.app.get(url(
685 response = self.app.get(url(
676 controller='pullrequests', action='index',
686 controller='pullrequests', action='index',
677 repo_name=target.repo_name))
687 repo_name=target.repo_name))
678 assert response.status_int == 200
688 assert response.status_int == 200
679 assert 'Pull request updated to' in response.body
689 assert 'Pull request updated to' in response.body
680 assert 'with 1 added, 1 removed commits.' in response.body
690 assert 'with 1 added, 1 removed commits.' in response.body
681
691
682 def test_update_of_ancestor_reference(self, backend, csrf_token):
692 def test_update_of_ancestor_reference(self, backend, csrf_token):
683 commits = [
693 commits = [
684 {'message': 'ancestor'},
694 {'message': 'ancestor'},
685 {'message': 'change'},
695 {'message': 'change'},
686 {'message': 'change-2'},
696 {'message': 'change-2'},
687 {'message': 'ancestor-new', 'parents': ['ancestor']},
697 {'message': 'ancestor-new', 'parents': ['ancestor']},
688 {'message': 'change-rebased'},
698 {'message': 'change-rebased'},
689 ]
699 ]
690 commit_ids = backend.create_master_repo(commits)
700 commit_ids = backend.create_master_repo(commits)
691 target = backend.create_repo(heads=['ancestor'])
701 target = backend.create_repo(heads=['ancestor'])
692 source = backend.create_repo(heads=['change'])
702 source = backend.create_repo(heads=['change'])
693
703
694 # create pr from a in source to A in target
704 # create pr from a in source to A in target
695 pull_request = PullRequest()
705 pull_request = PullRequest()
696 pull_request.source_repo = source
706 pull_request.source_repo = source
697 # TODO: johbo: Make sure that we write the source ref this way!
707 # TODO: johbo: Make sure that we write the source ref this way!
698 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
708 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
699 branch=backend.default_branch_name,
709 branch=backend.default_branch_name,
700 commit_id=commit_ids['change'])
710 commit_id=commit_ids['change'])
701 pull_request.target_repo = target
711 pull_request.target_repo = target
702 # TODO: johbo: Target ref should be branch based, since tip can jump
712 # TODO: johbo: Target ref should be branch based, since tip can jump
703 # from branch to branch
713 # from branch to branch
704 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
714 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
705 branch=backend.default_branch_name,
715 branch=backend.default_branch_name,
706 commit_id=commit_ids['ancestor'])
716 commit_id=commit_ids['ancestor'])
707 pull_request.revisions = [commit_ids['change']]
717 pull_request.revisions = [commit_ids['change']]
708 pull_request.title = u"Test"
718 pull_request.title = u"Test"
709 pull_request.description = u"Description"
719 pull_request.description = u"Description"
710 pull_request.author = UserModel().get_by_username(
720 pull_request.author = UserModel().get_by_username(
711 TEST_USER_ADMIN_LOGIN)
721 TEST_USER_ADMIN_LOGIN)
712 Session().add(pull_request)
722 Session().add(pull_request)
713 Session().commit()
723 Session().commit()
714 pull_request_id = pull_request.pull_request_id
724 pull_request_id = pull_request.pull_request_id
715
725
716 # target has ancestor - ancestor-new
726 # target has ancestor - ancestor-new
717 # source has ancestor - ancestor-new - change-rebased
727 # source has ancestor - ancestor-new - change-rebased
718 backend.pull_heads(target, heads=['ancestor-new'])
728 backend.pull_heads(target, heads=['ancestor-new'])
719 backend.pull_heads(source, heads=['change-rebased'])
729 backend.pull_heads(source, heads=['change-rebased'])
720
730
721 # update PR
731 # update PR
722 self.app.post(
732 self.app.post(
723 url(controller='pullrequests', action='update',
733 url(controller='pullrequests', action='update',
724 repo_name=target.repo_name,
734 repo_name=target.repo_name,
725 pull_request_id=str(pull_request_id)),
735 pull_request_id=str(pull_request_id)),
726 params={'update_commits': 'true', '_method': 'put',
736 params={'update_commits': 'true', '_method': 'put',
727 'csrf_token': csrf_token},
737 'csrf_token': csrf_token},
728 status=200)
738 status=200)
729
739
730 # Expect the target reference to be updated correctly
740 # Expect the target reference to be updated correctly
731 pull_request = PullRequest.get(pull_request_id)
741 pull_request = PullRequest.get(pull_request_id)
732 assert pull_request.revisions == [commit_ids['change-rebased']]
742 assert pull_request.revisions == [commit_ids['change-rebased']]
733 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
743 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
734 branch=backend.default_branch_name,
744 branch=backend.default_branch_name,
735 commit_id=commit_ids['ancestor-new'])
745 commit_id=commit_ids['ancestor-new'])
736 assert pull_request.target_ref == expected_target_ref
746 assert pull_request.target_ref == expected_target_ref
737
747
738 def test_remove_pull_request_branch(self, backend_git, csrf_token):
748 def test_remove_pull_request_branch(self, backend_git, csrf_token):
739 branch_name = 'development'
749 branch_name = 'development'
740 commits = [
750 commits = [
741 {'message': 'initial-commit'},
751 {'message': 'initial-commit'},
742 {'message': 'old-feature'},
752 {'message': 'old-feature'},
743 {'message': 'new-feature', 'branch': branch_name},
753 {'message': 'new-feature', 'branch': branch_name},
744 ]
754 ]
745 repo = backend_git.create_repo(commits)
755 repo = backend_git.create_repo(commits)
746 commit_ids = backend_git.commit_ids
756 commit_ids = backend_git.commit_ids
747
757
748 pull_request = PullRequest()
758 pull_request = PullRequest()
749 pull_request.source_repo = repo
759 pull_request.source_repo = repo
750 pull_request.target_repo = repo
760 pull_request.target_repo = repo
751 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
761 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
752 branch=branch_name, commit_id=commit_ids['new-feature'])
762 branch=branch_name, commit_id=commit_ids['new-feature'])
753 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
763 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
754 branch=backend_git.default_branch_name,
764 branch=backend_git.default_branch_name,
755 commit_id=commit_ids['old-feature'])
765 commit_id=commit_ids['old-feature'])
756 pull_request.revisions = [commit_ids['new-feature']]
766 pull_request.revisions = [commit_ids['new-feature']]
757 pull_request.title = u"Test"
767 pull_request.title = u"Test"
758 pull_request.description = u"Description"
768 pull_request.description = u"Description"
759 pull_request.author = UserModel().get_by_username(
769 pull_request.author = UserModel().get_by_username(
760 TEST_USER_ADMIN_LOGIN)
770 TEST_USER_ADMIN_LOGIN)
761 Session().add(pull_request)
771 Session().add(pull_request)
762 Session().commit()
772 Session().commit()
763
773
764 vcs = repo.scm_instance()
774 vcs = repo.scm_instance()
765 vcs.remove_ref('refs/heads/{}'.format(branch_name))
775 vcs.remove_ref('refs/heads/{}'.format(branch_name))
766
776
767 response = self.app.get(url(
777 response = self.app.get(url(
768 controller='pullrequests', action='show',
778 controller='pullrequests', action='show',
769 repo_name=repo.repo_name,
779 repo_name=repo.repo_name,
770 pull_request_id=str(pull_request.pull_request_id)))
780 pull_request_id=str(pull_request.pull_request_id)))
771
781
772 assert response.status_int == 200
782 assert response.status_int == 200
773 assert_response = AssertResponse(response)
783 assert_response = AssertResponse(response)
774 assert_response.element_contains(
784 assert_response.element_contains(
775 '#changeset_compare_view_content .alert strong',
785 '#changeset_compare_view_content .alert strong',
776 'Missing commits')
786 'Missing commits')
777 assert_response.element_contains(
787 assert_response.element_contains(
778 '#changeset_compare_view_content .alert',
788 '#changeset_compare_view_content .alert',
779 'This pull request cannot be displayed, because one or more'
789 'This pull request cannot be displayed, because one or more'
780 ' commits no longer exist in the source repository.')
790 ' commits no longer exist in the source repository.')
781
791
782 def test_strip_commits_from_pull_request(
792 def test_strip_commits_from_pull_request(
783 self, backend, pr_util, csrf_token):
793 self, backend, pr_util, csrf_token):
784 commits = [
794 commits = [
785 {'message': 'initial-commit'},
795 {'message': 'initial-commit'},
786 {'message': 'old-feature'},
796 {'message': 'old-feature'},
787 {'message': 'new-feature', 'parents': ['initial-commit']},
797 {'message': 'new-feature', 'parents': ['initial-commit']},
788 ]
798 ]
789 pull_request = pr_util.create_pull_request(
799 pull_request = pr_util.create_pull_request(
790 commits, target_head='initial-commit', source_head='new-feature',
800 commits, target_head='initial-commit', source_head='new-feature',
791 revisions=['new-feature'])
801 revisions=['new-feature'])
792
802
793 vcs = pr_util.source_repository.scm_instance()
803 vcs = pr_util.source_repository.scm_instance()
794 if backend.alias == 'git':
804 if backend.alias == 'git':
795 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
805 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
796 else:
806 else:
797 vcs.strip(pr_util.commit_ids['new-feature'])
807 vcs.strip(pr_util.commit_ids['new-feature'])
798
808
799 response = self.app.get(url(
809 response = self.app.get(url(
800 controller='pullrequests', action='show',
810 controller='pullrequests', action='show',
801 repo_name=pr_util.target_repository.repo_name,
811 repo_name=pr_util.target_repository.repo_name,
802 pull_request_id=str(pull_request.pull_request_id)))
812 pull_request_id=str(pull_request.pull_request_id)))
803
813
804 assert response.status_int == 200
814 assert response.status_int == 200
805 assert_response = AssertResponse(response)
815 assert_response = AssertResponse(response)
806 assert_response.element_contains(
816 assert_response.element_contains(
807 '#changeset_compare_view_content .alert strong',
817 '#changeset_compare_view_content .alert strong',
808 'Missing commits')
818 'Missing commits')
809 assert_response.element_contains(
819 assert_response.element_contains(
810 '#changeset_compare_view_content .alert',
820 '#changeset_compare_view_content .alert',
811 'This pull request cannot be displayed, because one or more'
821 'This pull request cannot be displayed, because one or more'
812 ' commits no longer exist in the source repository.')
822 ' commits no longer exist in the source repository.')
813 assert_response.element_contains(
823 assert_response.element_contains(
814 '#update_commits',
824 '#update_commits',
815 'Update commits')
825 'Update commits')
816
826
817 def test_strip_commits_and_update(
827 def test_strip_commits_and_update(
818 self, backend, pr_util, csrf_token):
828 self, backend, pr_util, csrf_token):
819 commits = [
829 commits = [
820 {'message': 'initial-commit'},
830 {'message': 'initial-commit'},
821 {'message': 'old-feature'},
831 {'message': 'old-feature'},
822 {'message': 'new-feature', 'parents': ['old-feature']},
832 {'message': 'new-feature', 'parents': ['old-feature']},
823 ]
833 ]
824 pull_request = pr_util.create_pull_request(
834 pull_request = pr_util.create_pull_request(
825 commits, target_head='old-feature', source_head='new-feature',
835 commits, target_head='old-feature', source_head='new-feature',
826 revisions=['new-feature'], mergeable=True)
836 revisions=['new-feature'], mergeable=True)
827
837
828 vcs = pr_util.source_repository.scm_instance()
838 vcs = pr_util.source_repository.scm_instance()
829 if backend.alias == 'git':
839 if backend.alias == 'git':
830 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
840 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
831 else:
841 else:
832 vcs.strip(pr_util.commit_ids['new-feature'])
842 vcs.strip(pr_util.commit_ids['new-feature'])
833
843
834 response = self.app.post(
844 response = self.app.post(
835 url(controller='pullrequests', action='update',
845 url(controller='pullrequests', action='update',
836 repo_name=pull_request.target_repo.repo_name,
846 repo_name=pull_request.target_repo.repo_name,
837 pull_request_id=str(pull_request.pull_request_id)),
847 pull_request_id=str(pull_request.pull_request_id)),
838 params={'update_commits': 'true', '_method': 'put',
848 params={'update_commits': 'true', '_method': 'put',
839 'csrf_token': csrf_token})
849 'csrf_token': csrf_token})
840
850
841 assert response.status_int == 200
851 assert response.status_int == 200
842 assert response.body == 'true'
852 assert response.body == 'true'
843
853
844 # Make sure that after update, it won't raise 500 errors
854 # Make sure that after update, it won't raise 500 errors
845 response = self.app.get(url(
855 response = self.app.get(url(
846 controller='pullrequests', action='show',
856 controller='pullrequests', action='show',
847 repo_name=pr_util.target_repository.repo_name,
857 repo_name=pr_util.target_repository.repo_name,
848 pull_request_id=str(pull_request.pull_request_id)))
858 pull_request_id=str(pull_request.pull_request_id)))
849
859
850 assert response.status_int == 200
860 assert response.status_int == 200
851 assert_response = AssertResponse(response)
861 assert_response = AssertResponse(response)
852 assert_response.element_contains(
862 assert_response.element_contains(
853 '#changeset_compare_view_content .alert strong',
863 '#changeset_compare_view_content .alert strong',
854 'Missing commits')
864 'Missing commits')
855
865
856 def test_branch_is_a_link(self, pr_util):
866 def test_branch_is_a_link(self, pr_util):
857 pull_request = pr_util.create_pull_request()
867 pull_request = pr_util.create_pull_request()
858 pull_request.source_ref = 'branch:origin:1234567890abcdef'
868 pull_request.source_ref = 'branch:origin:1234567890abcdef'
859 pull_request.target_ref = 'branch:target:abcdef1234567890'
869 pull_request.target_ref = 'branch:target:abcdef1234567890'
860 Session().add(pull_request)
870 Session().add(pull_request)
861 Session().commit()
871 Session().commit()
862
872
863 response = self.app.get(url(
873 response = self.app.get(url(
864 controller='pullrequests', action='show',
874 controller='pullrequests', action='show',
865 repo_name=pull_request.target_repo.scm_instance().name,
875 repo_name=pull_request.target_repo.scm_instance().name,
866 pull_request_id=str(pull_request.pull_request_id)))
876 pull_request_id=str(pull_request.pull_request_id)))
867 assert response.status_int == 200
877 assert response.status_int == 200
868 assert_response = AssertResponse(response)
878 assert_response = AssertResponse(response)
869
879
870 origin = assert_response.get_element('.pr-origininfo .tag')
880 origin = assert_response.get_element('.pr-origininfo .tag')
871 origin_children = origin.getchildren()
881 origin_children = origin.getchildren()
872 assert len(origin_children) == 1
882 assert len(origin_children) == 1
873 target = assert_response.get_element('.pr-targetinfo .tag')
883 target = assert_response.get_element('.pr-targetinfo .tag')
874 target_children = target.getchildren()
884 target_children = target.getchildren()
875 assert len(target_children) == 1
885 assert len(target_children) == 1
876
886
877 expected_origin_link = url(
887 expected_origin_link = url(
878 'changelog_home',
888 'changelog_home',
879 repo_name=pull_request.source_repo.scm_instance().name,
889 repo_name=pull_request.source_repo.scm_instance().name,
880 branch='origin')
890 branch='origin')
881 expected_target_link = url(
891 expected_target_link = url(
882 'changelog_home',
892 'changelog_home',
883 repo_name=pull_request.target_repo.scm_instance().name,
893 repo_name=pull_request.target_repo.scm_instance().name,
884 branch='target')
894 branch='target')
885 assert origin_children[0].attrib['href'] == expected_origin_link
895 assert origin_children[0].attrib['href'] == expected_origin_link
886 assert origin_children[0].text == 'branch: origin'
896 assert origin_children[0].text == 'branch: origin'
887 assert target_children[0].attrib['href'] == expected_target_link
897 assert target_children[0].attrib['href'] == expected_target_link
888 assert target_children[0].text == 'branch: target'
898 assert target_children[0].text == 'branch: target'
889
899
890 def test_bookmark_is_not_a_link(self, pr_util):
900 def test_bookmark_is_not_a_link(self, pr_util):
891 pull_request = pr_util.create_pull_request()
901 pull_request = pr_util.create_pull_request()
892 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
902 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
893 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
903 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
894 Session().add(pull_request)
904 Session().add(pull_request)
895 Session().commit()
905 Session().commit()
896
906
897 response = self.app.get(url(
907 response = self.app.get(url(
898 controller='pullrequests', action='show',
908 controller='pullrequests', action='show',
899 repo_name=pull_request.target_repo.scm_instance().name,
909 repo_name=pull_request.target_repo.scm_instance().name,
900 pull_request_id=str(pull_request.pull_request_id)))
910 pull_request_id=str(pull_request.pull_request_id)))
901 assert response.status_int == 200
911 assert response.status_int == 200
902 assert_response = AssertResponse(response)
912 assert_response = AssertResponse(response)
903
913
904 origin = assert_response.get_element('.pr-origininfo .tag')
914 origin = assert_response.get_element('.pr-origininfo .tag')
905 assert origin.text.strip() == 'bookmark: origin'
915 assert origin.text.strip() == 'bookmark: origin'
906 assert origin.getchildren() == []
916 assert origin.getchildren() == []
907
917
908 target = assert_response.get_element('.pr-targetinfo .tag')
918 target = assert_response.get_element('.pr-targetinfo .tag')
909 assert target.text.strip() == 'bookmark: target'
919 assert target.text.strip() == 'bookmark: target'
910 assert target.getchildren() == []
920 assert target.getchildren() == []
911
921
912 def test_tag_is_not_a_link(self, pr_util):
922 def test_tag_is_not_a_link(self, pr_util):
913 pull_request = pr_util.create_pull_request()
923 pull_request = pr_util.create_pull_request()
914 pull_request.source_ref = 'tag:origin:1234567890abcdef'
924 pull_request.source_ref = 'tag:origin:1234567890abcdef'
915 pull_request.target_ref = 'tag:target:abcdef1234567890'
925 pull_request.target_ref = 'tag:target:abcdef1234567890'
916 Session().add(pull_request)
926 Session().add(pull_request)
917 Session().commit()
927 Session().commit()
918
928
919 response = self.app.get(url(
929 response = self.app.get(url(
920 controller='pullrequests', action='show',
930 controller='pullrequests', action='show',
921 repo_name=pull_request.target_repo.scm_instance().name,
931 repo_name=pull_request.target_repo.scm_instance().name,
922 pull_request_id=str(pull_request.pull_request_id)))
932 pull_request_id=str(pull_request.pull_request_id)))
923 assert response.status_int == 200
933 assert response.status_int == 200
924 assert_response = AssertResponse(response)
934 assert_response = AssertResponse(response)
925
935
926 origin = assert_response.get_element('.pr-origininfo .tag')
936 origin = assert_response.get_element('.pr-origininfo .tag')
927 assert origin.text.strip() == 'tag: origin'
937 assert origin.text.strip() == 'tag: origin'
928 assert origin.getchildren() == []
938 assert origin.getchildren() == []
929
939
930 target = assert_response.get_element('.pr-targetinfo .tag')
940 target = assert_response.get_element('.pr-targetinfo .tag')
931 assert target.text.strip() == 'tag: target'
941 assert target.text.strip() == 'tag: target'
932 assert target.getchildren() == []
942 assert target.getchildren() == []
933
943
934 def test_description_is_escaped_on_index_page(self, backend, pr_util):
944 def test_description_is_escaped_on_index_page(self, backend, pr_util):
935 xss_description = "<script>alert('Hi!')</script>"
945 xss_description = "<script>alert('Hi!')</script>"
936 pull_request = pr_util.create_pull_request(description=xss_description)
946 pull_request = pr_util.create_pull_request(description=xss_description)
937 response = self.app.get(url(
947 response = self.app.get(url(
938 controller='pullrequests', action='show_all',
948 controller='pullrequests', action='show_all',
939 repo_name=pull_request.target_repo.repo_name))
949 repo_name=pull_request.target_repo.repo_name))
940 response.mustcontain(
950 response.mustcontain(
941 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
951 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
942
952
943 @pytest.mark.parametrize('mergeable', [True, False])
953 @pytest.mark.parametrize('mergeable', [True, False])
944 def test_shadow_repository_link(
954 def test_shadow_repository_link(
945 self, mergeable, pr_util, http_host_stub):
955 self, mergeable, pr_util, http_host_stub):
946 """
956 """
947 Check that the pull request summary page displays a link to the shadow
957 Check that the pull request summary page displays a link to the shadow
948 repository if the pull request is mergeable. If it is not mergeable
958 repository if the pull request is mergeable. If it is not mergeable
949 the link should not be displayed.
959 the link should not be displayed.
950 """
960 """
951 pull_request = pr_util.create_pull_request(
961 pull_request = pr_util.create_pull_request(
952 mergeable=mergeable, enable_notifications=False)
962 mergeable=mergeable, enable_notifications=False)
953 target_repo = pull_request.target_repo.scm_instance()
963 target_repo = pull_request.target_repo.scm_instance()
954 pr_id = pull_request.pull_request_id
964 pr_id = pull_request.pull_request_id
955 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
965 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
956 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
966 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
957
967
958 response = self.app.get(url(
968 response = self.app.get(url(
959 controller='pullrequests', action='show',
969 controller='pullrequests', action='show',
960 repo_name=target_repo.name,
970 repo_name=target_repo.name,
961 pull_request_id=str(pr_id)))
971 pull_request_id=str(pr_id)))
962
972
963 assertr = AssertResponse(response)
973 assertr = AssertResponse(response)
964 if mergeable:
974 if mergeable:
965 assertr.element_value_contains(
975 assertr.element_value_contains(
966 'div.pr-mergeinfo input', shadow_url)
976 'div.pr-mergeinfo input', shadow_url)
967 assertr.element_value_contains(
977 assertr.element_value_contains(
968 'div.pr-mergeinfo input', 'pr-merge')
978 'div.pr-mergeinfo input', 'pr-merge')
969 else:
979 else:
970 assertr.no_element_exists('div.pr-mergeinfo')
980 assertr.no_element_exists('div.pr-mergeinfo')
971
981
972
982
973 @pytest.mark.usefixtures('app')
983 @pytest.mark.usefixtures('app')
974 @pytest.mark.backends("git", "hg")
984 @pytest.mark.backends("git", "hg")
975 class TestPullrequestsControllerDelete(object):
985 class TestPullrequestsControllerDelete(object):
976 def test_pull_request_delete_button_permissions_admin(
986 def test_pull_request_delete_button_permissions_admin(
977 self, autologin_user, user_admin, pr_util):
987 self, autologin_user, user_admin, pr_util):
978 pull_request = pr_util.create_pull_request(
988 pull_request = pr_util.create_pull_request(
979 author=user_admin.username, enable_notifications=False)
989 author=user_admin.username, enable_notifications=False)
980
990
981 response = self.app.get(url(
991 response = self.app.get(url(
982 controller='pullrequests', action='show',
992 controller='pullrequests', action='show',
983 repo_name=pull_request.target_repo.scm_instance().name,
993 repo_name=pull_request.target_repo.scm_instance().name,
984 pull_request_id=str(pull_request.pull_request_id)))
994 pull_request_id=str(pull_request.pull_request_id)))
985
995
986 response.mustcontain('id="delete_pullrequest"')
996 response.mustcontain('id="delete_pullrequest"')
987 response.mustcontain('Confirm to delete this pull request')
997 response.mustcontain('Confirm to delete this pull request')
988
998
989 def test_pull_request_delete_button_permissions_owner(
999 def test_pull_request_delete_button_permissions_owner(
990 self, autologin_regular_user, user_regular, pr_util):
1000 self, autologin_regular_user, user_regular, pr_util):
991 pull_request = pr_util.create_pull_request(
1001 pull_request = pr_util.create_pull_request(
992 author=user_regular.username, enable_notifications=False)
1002 author=user_regular.username, enable_notifications=False)
993
1003
994 response = self.app.get(url(
1004 response = self.app.get(url(
995 controller='pullrequests', action='show',
1005 controller='pullrequests', action='show',
996 repo_name=pull_request.target_repo.scm_instance().name,
1006 repo_name=pull_request.target_repo.scm_instance().name,
997 pull_request_id=str(pull_request.pull_request_id)))
1007 pull_request_id=str(pull_request.pull_request_id)))
998
1008
999 response.mustcontain('id="delete_pullrequest"')
1009 response.mustcontain('id="delete_pullrequest"')
1000 response.mustcontain('Confirm to delete this pull request')
1010 response.mustcontain('Confirm to delete this pull request')
1001
1011
1002 def test_pull_request_delete_button_permissions_forbidden(
1012 def test_pull_request_delete_button_permissions_forbidden(
1003 self, autologin_regular_user, user_regular, user_admin, pr_util):
1013 self, autologin_regular_user, user_regular, user_admin, pr_util):
1004 pull_request = pr_util.create_pull_request(
1014 pull_request = pr_util.create_pull_request(
1005 author=user_admin.username, enable_notifications=False)
1015 author=user_admin.username, enable_notifications=False)
1006
1016
1007 response = self.app.get(url(
1017 response = self.app.get(url(
1008 controller='pullrequests', action='show',
1018 controller='pullrequests', action='show',
1009 repo_name=pull_request.target_repo.scm_instance().name,
1019 repo_name=pull_request.target_repo.scm_instance().name,
1010 pull_request_id=str(pull_request.pull_request_id)))
1020 pull_request_id=str(pull_request.pull_request_id)))
1011 response.mustcontain(no=['id="delete_pullrequest"'])
1021 response.mustcontain(no=['id="delete_pullrequest"'])
1012 response.mustcontain(no=['Confirm to delete this pull request'])
1022 response.mustcontain(no=['Confirm to delete this pull request'])
1013
1023
1014 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1024 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1015 self, autologin_regular_user, user_regular, user_admin, pr_util,
1025 self, autologin_regular_user, user_regular, user_admin, pr_util,
1016 user_util):
1026 user_util):
1017
1027
1018 pull_request = pr_util.create_pull_request(
1028 pull_request = pr_util.create_pull_request(
1019 author=user_admin.username, enable_notifications=False)
1029 author=user_admin.username, enable_notifications=False)
1020
1030
1021 user_util.grant_user_permission_to_repo(
1031 user_util.grant_user_permission_to_repo(
1022 pull_request.target_repo, user_regular,
1032 pull_request.target_repo, user_regular,
1023 'repository.write')
1033 'repository.write')
1024
1034
1025 response = self.app.get(url(
1035 response = self.app.get(url(
1026 controller='pullrequests', action='show',
1036 controller='pullrequests', action='show',
1027 repo_name=pull_request.target_repo.scm_instance().name,
1037 repo_name=pull_request.target_repo.scm_instance().name,
1028 pull_request_id=str(pull_request.pull_request_id)))
1038 pull_request_id=str(pull_request.pull_request_id)))
1029
1039
1030 response.mustcontain('id="open_edit_pullrequest"')
1040 response.mustcontain('id="open_edit_pullrequest"')
1031 response.mustcontain('id="delete_pullrequest"')
1041 response.mustcontain('id="delete_pullrequest"')
1032 response.mustcontain(no=['Confirm to delete this pull request'])
1042 response.mustcontain(no=['Confirm to delete this pull request'])
1033
1043
1034
1044
1035 def assert_pull_request_status(pull_request, expected_status):
1045 def assert_pull_request_status(pull_request, expected_status):
1036 status = ChangesetStatusModel().calculated_review_status(
1046 status = ChangesetStatusModel().calculated_review_status(
1037 pull_request=pull_request)
1047 pull_request=pull_request)
1038 assert status == expected_status
1048 assert status == expected_status
1039
1049
1040
1050
1041 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
1051 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
1042 @pytest.mark.usefixtures("autologin_user")
1052 @pytest.mark.usefixtures("autologin_user")
1043 def test_redirects_to_repo_summary_for_svn_repositories(
1053 def test_redirects_to_repo_summary_for_svn_repositories(
1044 backend_svn, app, action):
1054 backend_svn, app, action):
1045 denied_actions = ['show_all', 'index', 'create']
1055 denied_actions = ['show_all', 'index', 'create']
1046 for action in denied_actions:
1056 for action in denied_actions:
1047 response = app.get(url(
1057 response = app.get(url(
1048 controller='pullrequests', action=action,
1058 controller='pullrequests', action=action,
1049 repo_name=backend_svn.repo_name))
1059 repo_name=backend_svn.repo_name))
1050 assert response.status_int == 302
1060 assert response.status_int == 302
1051
1061
1052 # Not allowed, redirect to the summary
1062 # Not allowed, redirect to the summary
1053 redirected = response.follow()
1063 redirected = response.follow()
1054 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
1064 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
1055
1065
1056 # URL adds leading slash and path doesn't have it
1066 # URL adds leading slash and path doesn't have it
1057 assert redirected.req.path == summary_url
1067 assert redirected.req.path == summary_url
1058
1068
1059
1069
1060 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1070 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1061 # TODO: johbo: Global import not possible because models.forms blows up
1071 # TODO: johbo: Global import not possible because models.forms blows up
1062 from rhodecode.controllers.pullrequests import PullrequestsController
1072 from rhodecode.controllers.pullrequests import PullrequestsController
1063 controller = PullrequestsController()
1073 controller = PullrequestsController()
1064 patcher = mock.patch(
1074 patcher = mock.patch(
1065 'rhodecode.model.db.BaseModel.get', return_value=None)
1075 'rhodecode.model.db.BaseModel.get', return_value=None)
1066 with pytest.raises(HTTPNotFound), patcher:
1076 with pytest.raises(HTTPNotFound), patcher:
1067 controller._delete_comment(1)
1077 controller._delete_comment(1)
General Comments 0
You need to be logged in to leave comments. Login now