##// END OF EJS Templates
pull-requests: show common ancestor inside pull-request view....
marcink -
r1594:d24b008b default
parent child Browse files
Show More
@@ -1,1060 +1,1071 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
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
74
74
75 def _extract_ordering(self, request):
75 def _extract_ordering(self, request):
76 column_index = safe_int(request.GET.get('order[0][column]'))
76 column_index = safe_int(request.GET.get('order[0][column]'))
77 order_dir = request.GET.get('order[0][dir]', 'desc')
77 order_dir = request.GET.get('order[0][dir]', 'desc')
78 order_by = request.GET.get(
78 order_by = request.GET.get(
79 'columns[%s][data][sort]' % column_index, 'name_raw')
79 'columns[%s][data][sort]' % column_index, 'name_raw')
80 return order_by, order_dir
80 return order_by, order_dir
81
81
82 @LoginRequired()
82 @LoginRequired()
83 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
83 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
84 'repository.admin')
84 'repository.admin')
85 @HasAcceptedRepoType('git', 'hg')
85 @HasAcceptedRepoType('git', 'hg')
86 def show_all(self, repo_name):
86 def show_all(self, repo_name):
87 # filter types
87 # filter types
88 c.active = 'open'
88 c.active = 'open'
89 c.source = str2bool(request.GET.get('source'))
89 c.source = str2bool(request.GET.get('source'))
90 c.closed = str2bool(request.GET.get('closed'))
90 c.closed = str2bool(request.GET.get('closed'))
91 c.my = str2bool(request.GET.get('my'))
91 c.my = str2bool(request.GET.get('my'))
92 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
92 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
93 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
93 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
94 c.repo_name = repo_name
94 c.repo_name = repo_name
95
95
96 opened_by = None
96 opened_by = None
97 if c.my:
97 if c.my:
98 c.active = 'my'
98 c.active = 'my'
99 opened_by = [c.rhodecode_user.user_id]
99 opened_by = [c.rhodecode_user.user_id]
100
100
101 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
101 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
102 if c.closed:
102 if c.closed:
103 c.active = 'closed'
103 c.active = 'closed'
104 statuses = [PullRequest.STATUS_CLOSED]
104 statuses = [PullRequest.STATUS_CLOSED]
105
105
106 if c.awaiting_review and not c.source:
106 if c.awaiting_review and not c.source:
107 c.active = 'awaiting'
107 c.active = 'awaiting'
108 if c.source and not c.awaiting_review:
108 if c.source and not c.awaiting_review:
109 c.active = 'source'
109 c.active = 'source'
110 if c.awaiting_my_review:
110 if c.awaiting_my_review:
111 c.active = 'awaiting_my'
111 c.active = 'awaiting_my'
112
112
113 data = self._get_pull_requests_list(
113 data = self._get_pull_requests_list(
114 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
114 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
115 if not request.is_xhr:
115 if not request.is_xhr:
116 c.data = json.dumps(data['data'])
116 c.data = json.dumps(data['data'])
117 c.records_total = data['recordsTotal']
117 c.records_total = data['recordsTotal']
118 return render('/pullrequests/pullrequests.mako')
118 return render('/pullrequests/pullrequests.mako')
119 else:
119 else:
120 return json.dumps(data)
120 return json.dumps(data)
121
121
122 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
122 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
123 # pagination
123 # pagination
124 start = safe_int(request.GET.get('start'), 0)
124 start = safe_int(request.GET.get('start'), 0)
125 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
125 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
126 order_by, order_dir = self._extract_ordering(request)
126 order_by, order_dir = self._extract_ordering(request)
127
127
128 if c.awaiting_review:
128 if c.awaiting_review:
129 pull_requests = PullRequestModel().get_awaiting_review(
129 pull_requests = PullRequestModel().get_awaiting_review(
130 repo_name, source=c.source, opened_by=opened_by,
130 repo_name, source=c.source, opened_by=opened_by,
131 statuses=statuses, offset=start, length=length,
131 statuses=statuses, offset=start, length=length,
132 order_by=order_by, order_dir=order_dir)
132 order_by=order_by, order_dir=order_dir)
133 pull_requests_total_count = PullRequestModel(
133 pull_requests_total_count = PullRequestModel(
134 ).count_awaiting_review(
134 ).count_awaiting_review(
135 repo_name, source=c.source, statuses=statuses,
135 repo_name, source=c.source, statuses=statuses,
136 opened_by=opened_by)
136 opened_by=opened_by)
137 elif c.awaiting_my_review:
137 elif c.awaiting_my_review:
138 pull_requests = PullRequestModel().get_awaiting_my_review(
138 pull_requests = PullRequestModel().get_awaiting_my_review(
139 repo_name, source=c.source, opened_by=opened_by,
139 repo_name, source=c.source, opened_by=opened_by,
140 user_id=c.rhodecode_user.user_id, statuses=statuses,
140 user_id=c.rhodecode_user.user_id, statuses=statuses,
141 offset=start, length=length, order_by=order_by,
141 offset=start, length=length, order_by=order_by,
142 order_dir=order_dir)
142 order_dir=order_dir)
143 pull_requests_total_count = PullRequestModel(
143 pull_requests_total_count = PullRequestModel(
144 ).count_awaiting_my_review(
144 ).count_awaiting_my_review(
145 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,
146 statuses=statuses, opened_by=opened_by)
146 statuses=statuses, opened_by=opened_by)
147 else:
147 else:
148 pull_requests = PullRequestModel().get_all(
148 pull_requests = PullRequestModel().get_all(
149 repo_name, source=c.source, opened_by=opened_by,
149 repo_name, source=c.source, opened_by=opened_by,
150 statuses=statuses, offset=start, length=length,
150 statuses=statuses, offset=start, length=length,
151 order_by=order_by, order_dir=order_dir)
151 order_by=order_by, order_dir=order_dir)
152 pull_requests_total_count = PullRequestModel().count_all(
152 pull_requests_total_count = PullRequestModel().count_all(
153 repo_name, source=c.source, statuses=statuses,
153 repo_name, source=c.source, statuses=statuses,
154 opened_by=opened_by)
154 opened_by=opened_by)
155
155
156 from rhodecode.lib.utils import PartialRenderer
156 from rhodecode.lib.utils import PartialRenderer
157 _render = PartialRenderer('data_table/_dt_elements.mako')
157 _render = PartialRenderer('data_table/_dt_elements.mako')
158 data = []
158 data = []
159 for pr in pull_requests:
159 for pr in pull_requests:
160 comments = CommentsModel().get_all_comments(
160 comments = CommentsModel().get_all_comments(
161 c.rhodecode_db_repo.repo_id, pull_request=pr)
161 c.rhodecode_db_repo.repo_id, pull_request=pr)
162
162
163 data.append({
163 data.append({
164 'name': _render('pullrequest_name',
164 'name': _render('pullrequest_name',
165 pr.pull_request_id, pr.target_repo.repo_name),
165 pr.pull_request_id, pr.target_repo.repo_name),
166 'name_raw': pr.pull_request_id,
166 'name_raw': pr.pull_request_id,
167 'status': _render('pullrequest_status',
167 'status': _render('pullrequest_status',
168 pr.calculated_review_status()),
168 pr.calculated_review_status()),
169 'title': _render(
169 'title': _render(
170 'pullrequest_title', pr.title, pr.description),
170 'pullrequest_title', pr.title, pr.description),
171 'description': h.escape(pr.description),
171 'description': h.escape(pr.description),
172 'updated_on': _render('pullrequest_updated_on',
172 'updated_on': _render('pullrequest_updated_on',
173 h.datetime_to_time(pr.updated_on)),
173 h.datetime_to_time(pr.updated_on)),
174 'updated_on_raw': h.datetime_to_time(pr.updated_on),
174 'updated_on_raw': h.datetime_to_time(pr.updated_on),
175 'created_on': _render('pullrequest_updated_on',
175 'created_on': _render('pullrequest_updated_on',
176 h.datetime_to_time(pr.created_on)),
176 h.datetime_to_time(pr.created_on)),
177 'created_on_raw': h.datetime_to_time(pr.created_on),
177 'created_on_raw': h.datetime_to_time(pr.created_on),
178 'author': _render('pullrequest_author',
178 'author': _render('pullrequest_author',
179 pr.author.full_contact, ),
179 pr.author.full_contact, ),
180 'author_raw': pr.author.full_name,
180 'author_raw': pr.author.full_name,
181 'comments': _render('pullrequest_comments', len(comments)),
181 'comments': _render('pullrequest_comments', len(comments)),
182 'comments_raw': len(comments),
182 'comments_raw': len(comments),
183 'closed': pr.is_closed(),
183 'closed': pr.is_closed(),
184 })
184 })
185 # json used to render the grid
185 # json used to render the grid
186 data = ({
186 data = ({
187 'data': data,
187 'data': data,
188 'recordsTotal': pull_requests_total_count,
188 'recordsTotal': pull_requests_total_count,
189 'recordsFiltered': pull_requests_total_count,
189 'recordsFiltered': pull_requests_total_count,
190 })
190 })
191 return data
191 return data
192
192
193 @LoginRequired()
193 @LoginRequired()
194 @NotAnonymous()
194 @NotAnonymous()
195 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
195 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
196 'repository.admin')
196 'repository.admin')
197 @HasAcceptedRepoType('git', 'hg')
197 @HasAcceptedRepoType('git', 'hg')
198 def index(self):
198 def index(self):
199 source_repo = c.rhodecode_db_repo
199 source_repo = c.rhodecode_db_repo
200
200
201 try:
201 try:
202 source_repo.scm_instance().get_commit()
202 source_repo.scm_instance().get_commit()
203 except EmptyRepositoryError:
203 except EmptyRepositoryError:
204 h.flash(h.literal(_('There are no commits yet')),
204 h.flash(h.literal(_('There are no commits yet')),
205 category='warning')
205 category='warning')
206 redirect(url('summary_home', repo_name=source_repo.repo_name))
206 redirect(url('summary_home', repo_name=source_repo.repo_name))
207
207
208 commit_id = request.GET.get('commit')
208 commit_id = request.GET.get('commit')
209 branch_ref = request.GET.get('branch')
209 branch_ref = request.GET.get('branch')
210 bookmark_ref = request.GET.get('bookmark')
210 bookmark_ref = request.GET.get('bookmark')
211
211
212 try:
212 try:
213 source_repo_data = PullRequestModel().generate_repo_data(
213 source_repo_data = PullRequestModel().generate_repo_data(
214 source_repo, commit_id=commit_id,
214 source_repo, commit_id=commit_id,
215 branch=branch_ref, bookmark=bookmark_ref)
215 branch=branch_ref, bookmark=bookmark_ref)
216 except CommitDoesNotExistError as e:
216 except CommitDoesNotExistError as e:
217 log.exception(e)
217 log.exception(e)
218 h.flash(_('Commit does not exist'), 'error')
218 h.flash(_('Commit does not exist'), 'error')
219 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
219 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
220
220
221 default_target_repo = source_repo
221 default_target_repo = source_repo
222
222
223 if source_repo.parent:
223 if source_repo.parent:
224 parent_vcs_obj = source_repo.parent.scm_instance()
224 parent_vcs_obj = source_repo.parent.scm_instance()
225 if parent_vcs_obj and not parent_vcs_obj.is_empty():
225 if parent_vcs_obj and not parent_vcs_obj.is_empty():
226 # change default if we have a parent repo
226 # change default if we have a parent repo
227 default_target_repo = source_repo.parent
227 default_target_repo = source_repo.parent
228
228
229 target_repo_data = PullRequestModel().generate_repo_data(
229 target_repo_data = PullRequestModel().generate_repo_data(
230 default_target_repo)
230 default_target_repo)
231
231
232 selected_source_ref = source_repo_data['refs']['selected_ref']
232 selected_source_ref = source_repo_data['refs']['selected_ref']
233
233
234 title_source_ref = selected_source_ref.split(':', 2)[1]
234 title_source_ref = selected_source_ref.split(':', 2)[1]
235 c.default_title = PullRequestModel().generate_pullrequest_title(
235 c.default_title = PullRequestModel().generate_pullrequest_title(
236 source=source_repo.repo_name,
236 source=source_repo.repo_name,
237 source_ref=title_source_ref,
237 source_ref=title_source_ref,
238 target=default_target_repo.repo_name
238 target=default_target_repo.repo_name
239 )
239 )
240
240
241 c.default_repo_data = {
241 c.default_repo_data = {
242 'source_repo_name': source_repo.repo_name,
242 'source_repo_name': source_repo.repo_name,
243 'source_refs_json': json.dumps(source_repo_data),
243 'source_refs_json': json.dumps(source_repo_data),
244 'target_repo_name': default_target_repo.repo_name,
244 'target_repo_name': default_target_repo.repo_name,
245 'target_refs_json': json.dumps(target_repo_data),
245 'target_refs_json': json.dumps(target_repo_data),
246 }
246 }
247 c.default_source_ref = selected_source_ref
247 c.default_source_ref = selected_source_ref
248
248
249 return render('/pullrequests/pullrequest.mako')
249 return render('/pullrequests/pullrequest.mako')
250
250
251 @LoginRequired()
251 @LoginRequired()
252 @NotAnonymous()
252 @NotAnonymous()
253 @XHRRequired()
253 @XHRRequired()
254 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
254 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
255 'repository.admin')
255 'repository.admin')
256 @jsonify
256 @jsonify
257 def get_repo_refs(self, repo_name, target_repo_name):
257 def get_repo_refs(self, repo_name, target_repo_name):
258 repo = Repository.get_by_repo_name(target_repo_name)
258 repo = Repository.get_by_repo_name(target_repo_name)
259 if not repo:
259 if not repo:
260 raise HTTPNotFound
260 raise HTTPNotFound
261 return PullRequestModel().generate_repo_data(repo)
261 return PullRequestModel().generate_repo_data(repo)
262
262
263 @LoginRequired()
263 @LoginRequired()
264 @NotAnonymous()
264 @NotAnonymous()
265 @XHRRequired()
265 @XHRRequired()
266 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
266 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
267 'repository.admin')
267 'repository.admin')
268 @jsonify
268 @jsonify
269 def get_repo_destinations(self, repo_name):
269 def get_repo_destinations(self, repo_name):
270 repo = Repository.get_by_repo_name(repo_name)
270 repo = Repository.get_by_repo_name(repo_name)
271 if not repo:
271 if not repo:
272 raise HTTPNotFound
272 raise HTTPNotFound
273 filter_query = request.GET.get('query')
273 filter_query = request.GET.get('query')
274
274
275 query = Repository.query() \
275 query = Repository.query() \
276 .order_by(func.length(Repository.repo_name)) \
276 .order_by(func.length(Repository.repo_name)) \
277 .filter(or_(
277 .filter(or_(
278 Repository.repo_name == repo.repo_name,
278 Repository.repo_name == repo.repo_name,
279 Repository.fork_id == repo.repo_id))
279 Repository.fork_id == repo.repo_id))
280
280
281 if filter_query:
281 if filter_query:
282 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
282 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
283 query = query.filter(
283 query = query.filter(
284 Repository.repo_name.ilike(ilike_expression))
284 Repository.repo_name.ilike(ilike_expression))
285
285
286 add_parent = False
286 add_parent = False
287 if repo.parent:
287 if repo.parent:
288 if filter_query in repo.parent.repo_name:
288 if filter_query in repo.parent.repo_name:
289 parent_vcs_obj = repo.parent.scm_instance()
289 parent_vcs_obj = repo.parent.scm_instance()
290 if parent_vcs_obj and not parent_vcs_obj.is_empty():
290 if parent_vcs_obj and not parent_vcs_obj.is_empty():
291 add_parent = True
291 add_parent = True
292
292
293 limit = 20 - 1 if add_parent else 20
293 limit = 20 - 1 if add_parent else 20
294 all_repos = query.limit(limit).all()
294 all_repos = query.limit(limit).all()
295 if add_parent:
295 if add_parent:
296 all_repos += [repo.parent]
296 all_repos += [repo.parent]
297
297
298 repos = []
298 repos = []
299 for obj in self.scm_model.get_repos(all_repos):
299 for obj in self.scm_model.get_repos(all_repos):
300 repos.append({
300 repos.append({
301 'id': obj['name'],
301 'id': obj['name'],
302 'text': obj['name'],
302 'text': obj['name'],
303 'type': 'repo',
303 'type': 'repo',
304 'obj': obj['dbrepo']
304 'obj': obj['dbrepo']
305 })
305 })
306
306
307 data = {
307 data = {
308 'more': False,
308 'more': False,
309 'results': [{
309 'results': [{
310 'text': _('Repositories'),
310 'text': _('Repositories'),
311 'children': repos
311 'children': repos
312 }] if repos else []
312 }] if repos else []
313 }
313 }
314 return data
314 return data
315
315
316 @LoginRequired()
316 @LoginRequired()
317 @NotAnonymous()
317 @NotAnonymous()
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 'repository.admin')
319 'repository.admin')
320 @HasAcceptedRepoType('git', 'hg')
320 @HasAcceptedRepoType('git', 'hg')
321 @auth.CSRFRequired()
321 @auth.CSRFRequired()
322 def create(self, repo_name):
322 def create(self, repo_name):
323 repo = Repository.get_by_repo_name(repo_name)
323 repo = Repository.get_by_repo_name(repo_name)
324 if not repo:
324 if not repo:
325 raise HTTPNotFound
325 raise HTTPNotFound
326
326
327 controls = peppercorn.parse(request.POST.items())
327 controls = peppercorn.parse(request.POST.items())
328
328
329 try:
329 try:
330 _form = PullRequestForm(repo.repo_id)().to_python(controls)
330 _form = PullRequestForm(repo.repo_id)().to_python(controls)
331 except formencode.Invalid as errors:
331 except formencode.Invalid as errors:
332 if errors.error_dict.get('revisions'):
332 if errors.error_dict.get('revisions'):
333 msg = 'Revisions: %s' % errors.error_dict['revisions']
333 msg = 'Revisions: %s' % errors.error_dict['revisions']
334 elif errors.error_dict.get('pullrequest_title'):
334 elif errors.error_dict.get('pullrequest_title'):
335 msg = _('Pull request requires a title with min. 3 chars')
335 msg = _('Pull request requires a title with min. 3 chars')
336 else:
336 else:
337 msg = _('Error creating pull request: {}').format(errors)
337 msg = _('Error creating pull request: {}').format(errors)
338 log.exception(msg)
338 log.exception(msg)
339 h.flash(msg, 'error')
339 h.flash(msg, 'error')
340
340
341 # would rather just go back to form ...
341 # would rather just go back to form ...
342 return redirect(url('pullrequest_home', repo_name=repo_name))
342 return redirect(url('pullrequest_home', repo_name=repo_name))
343
343
344 source_repo = _form['source_repo']
344 source_repo = _form['source_repo']
345 source_ref = _form['source_ref']
345 source_ref = _form['source_ref']
346 target_repo = _form['target_repo']
346 target_repo = _form['target_repo']
347 target_ref = _form['target_ref']
347 target_ref = _form['target_ref']
348 commit_ids = _form['revisions'][::-1]
348 commit_ids = _form['revisions'][::-1]
349 reviewers = [
349 reviewers = [
350 (r['user_id'], r['reasons']) for r in _form['review_members']]
350 (r['user_id'], r['reasons']) for r in _form['review_members']]
351
351
352 # find the ancestor for this pr
352 # find the ancestor for this pr
353 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
353 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
354 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
354 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
355
355
356 source_scm = source_db_repo.scm_instance()
356 source_scm = source_db_repo.scm_instance()
357 target_scm = target_db_repo.scm_instance()
357 target_scm = target_db_repo.scm_instance()
358
358
359 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
359 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
360 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
360 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
361
361
362 ancestor = source_scm.get_common_ancestor(
362 ancestor = source_scm.get_common_ancestor(
363 source_commit.raw_id, target_commit.raw_id, target_scm)
363 source_commit.raw_id, target_commit.raw_id, target_scm)
364
364
365 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
365 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
366 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
366 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
367
367
368 pullrequest_title = _form['pullrequest_title']
368 pullrequest_title = _form['pullrequest_title']
369 title_source_ref = source_ref.split(':', 2)[1]
369 title_source_ref = source_ref.split(':', 2)[1]
370 if not pullrequest_title:
370 if not pullrequest_title:
371 pullrequest_title = PullRequestModel().generate_pullrequest_title(
371 pullrequest_title = PullRequestModel().generate_pullrequest_title(
372 source=source_repo,
372 source=source_repo,
373 source_ref=title_source_ref,
373 source_ref=title_source_ref,
374 target=target_repo
374 target=target_repo
375 )
375 )
376
376
377 description = _form['pullrequest_desc']
377 description = _form['pullrequest_desc']
378 try:
378 try:
379 pull_request = PullRequestModel().create(
379 pull_request = PullRequestModel().create(
380 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
380 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
381 target_ref, commit_ids, reviewers, pullrequest_title,
381 target_ref, commit_ids, reviewers, pullrequest_title,
382 description
382 description
383 )
383 )
384 Session().commit()
384 Session().commit()
385 h.flash(_('Successfully opened new pull request'),
385 h.flash(_('Successfully opened new pull request'),
386 category='success')
386 category='success')
387 except Exception as e:
387 except Exception as e:
388 msg = _('Error occurred during sending pull request')
388 msg = _('Error occurred during sending pull request')
389 log.exception(msg)
389 log.exception(msg)
390 h.flash(msg, category='error')
390 h.flash(msg, category='error')
391 return redirect(url('pullrequest_home', repo_name=repo_name))
391 return redirect(url('pullrequest_home', repo_name=repo_name))
392
392
393 return redirect(url('pullrequest_show', repo_name=target_repo,
393 return redirect(url('pullrequest_show', repo_name=target_repo,
394 pull_request_id=pull_request.pull_request_id))
394 pull_request_id=pull_request.pull_request_id))
395
395
396 @LoginRequired()
396 @LoginRequired()
397 @NotAnonymous()
397 @NotAnonymous()
398 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
398 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
399 'repository.admin')
399 'repository.admin')
400 @auth.CSRFRequired()
400 @auth.CSRFRequired()
401 @jsonify
401 @jsonify
402 def update(self, repo_name, pull_request_id):
402 def update(self, repo_name, pull_request_id):
403 pull_request_id = safe_int(pull_request_id)
403 pull_request_id = safe_int(pull_request_id)
404 pull_request = PullRequest.get_or_404(pull_request_id)
404 pull_request = PullRequest.get_or_404(pull_request_id)
405 # only owner or admin can update it
405 # only owner or admin can update it
406 allowed_to_update = PullRequestModel().check_user_update(
406 allowed_to_update = PullRequestModel().check_user_update(
407 pull_request, c.rhodecode_user)
407 pull_request, c.rhodecode_user)
408 if allowed_to_update:
408 if allowed_to_update:
409 controls = peppercorn.parse(request.POST.items())
409 controls = peppercorn.parse(request.POST.items())
410
410
411 if 'review_members' in controls:
411 if 'review_members' in controls:
412 self._update_reviewers(
412 self._update_reviewers(
413 pull_request_id, controls['review_members'])
413 pull_request_id, controls['review_members'])
414 elif str2bool(request.POST.get('update_commits', 'false')):
414 elif str2bool(request.POST.get('update_commits', 'false')):
415 self._update_commits(pull_request)
415 self._update_commits(pull_request)
416 elif str2bool(request.POST.get('close_pull_request', 'false')):
416 elif str2bool(request.POST.get('close_pull_request', 'false')):
417 self._reject_close(pull_request)
417 self._reject_close(pull_request)
418 elif str2bool(request.POST.get('edit_pull_request', 'false')):
418 elif str2bool(request.POST.get('edit_pull_request', 'false')):
419 self._edit_pull_request(pull_request)
419 self._edit_pull_request(pull_request)
420 else:
420 else:
421 raise HTTPBadRequest()
421 raise HTTPBadRequest()
422 return True
422 return True
423 raise HTTPForbidden()
423 raise HTTPForbidden()
424
424
425 def _edit_pull_request(self, pull_request):
425 def _edit_pull_request(self, pull_request):
426 try:
426 try:
427 PullRequestModel().edit(
427 PullRequestModel().edit(
428 pull_request, request.POST.get('title'),
428 pull_request, request.POST.get('title'),
429 request.POST.get('description'))
429 request.POST.get('description'))
430 except ValueError:
430 except ValueError:
431 msg = _(u'Cannot update closed pull requests.')
431 msg = _(u'Cannot update closed pull requests.')
432 h.flash(msg, category='error')
432 h.flash(msg, category='error')
433 return
433 return
434 else:
434 else:
435 Session().commit()
435 Session().commit()
436
436
437 msg = _(u'Pull request title & description updated.')
437 msg = _(u'Pull request title & description updated.')
438 h.flash(msg, category='success')
438 h.flash(msg, category='success')
439 return
439 return
440
440
441 def _update_commits(self, pull_request):
441 def _update_commits(self, pull_request):
442 resp = PullRequestModel().update_commits(pull_request)
442 resp = PullRequestModel().update_commits(pull_request)
443
443
444 if resp.executed:
444 if resp.executed:
445 msg = _(
445 msg = _(
446 u'Pull request updated to "{source_commit_id}" with '
446 u'Pull request updated to "{source_commit_id}" with '
447 u'{count_added} added, {count_removed} removed commits.')
447 u'{count_added} added, {count_removed} removed commits.')
448 msg = msg.format(
448 msg = msg.format(
449 source_commit_id=pull_request.source_ref_parts.commit_id,
449 source_commit_id=pull_request.source_ref_parts.commit_id,
450 count_added=len(resp.changes.added),
450 count_added=len(resp.changes.added),
451 count_removed=len(resp.changes.removed))
451 count_removed=len(resp.changes.removed))
452 h.flash(msg, category='success')
452 h.flash(msg, category='success')
453
453
454 registry = get_current_registry()
454 registry = get_current_registry()
455 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
455 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
456 channelstream_config = rhodecode_plugins.get('channelstream', {})
456 channelstream_config = rhodecode_plugins.get('channelstream', {})
457 if channelstream_config.get('enabled'):
457 if channelstream_config.get('enabled'):
458 message = msg + (
458 message = msg + (
459 ' - <a onclick="window.location.reload()">'
459 ' - <a onclick="window.location.reload()">'
460 '<strong>{}</strong></a>'.format(_('Reload page')))
460 '<strong>{}</strong></a>'.format(_('Reload page')))
461 channel = '/repo${}$/pr/{}'.format(
461 channel = '/repo${}$/pr/{}'.format(
462 pull_request.target_repo.repo_name,
462 pull_request.target_repo.repo_name,
463 pull_request.pull_request_id
463 pull_request.pull_request_id
464 )
464 )
465 payload = {
465 payload = {
466 'type': 'message',
466 'type': 'message',
467 'user': 'system',
467 'user': 'system',
468 'exclude_users': [request.user.username],
468 'exclude_users': [request.user.username],
469 'channel': channel,
469 'channel': channel,
470 'message': {
470 'message': {
471 'message': message,
471 'message': message,
472 'level': 'success',
472 'level': 'success',
473 'topic': '/notifications'
473 'topic': '/notifications'
474 }
474 }
475 }
475 }
476 channelstream_request(
476 channelstream_request(
477 channelstream_config, [payload], '/message',
477 channelstream_config, [payload], '/message',
478 raise_exc=False)
478 raise_exc=False)
479 else:
479 else:
480 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
480 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
481 warning_reasons = [
481 warning_reasons = [
482 UpdateFailureReason.NO_CHANGE,
482 UpdateFailureReason.NO_CHANGE,
483 UpdateFailureReason.WRONG_REF_TPYE,
483 UpdateFailureReason.WRONG_REF_TPYE,
484 ]
484 ]
485 category = 'warning' if resp.reason in warning_reasons else 'error'
485 category = 'warning' if resp.reason in warning_reasons else 'error'
486 h.flash(msg, category=category)
486 h.flash(msg, category=category)
487
487
488 @auth.CSRFRequired()
488 @auth.CSRFRequired()
489 @LoginRequired()
489 @LoginRequired()
490 @NotAnonymous()
490 @NotAnonymous()
491 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
491 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
492 'repository.admin')
492 'repository.admin')
493 def merge(self, repo_name, pull_request_id):
493 def merge(self, repo_name, pull_request_id):
494 """
494 """
495 POST /{repo_name}/pull-request/{pull_request_id}
495 POST /{repo_name}/pull-request/{pull_request_id}
496
496
497 Merge will perform a server-side merge of the specified
497 Merge will perform a server-side merge of the specified
498 pull request, if the pull request is approved and mergeable.
498 pull request, if the pull request is approved and mergeable.
499 After successful merging, the pull request is automatically
499 After successful merging, the pull request is automatically
500 closed, with a relevant comment.
500 closed, with a relevant comment.
501 """
501 """
502 pull_request_id = safe_int(pull_request_id)
502 pull_request_id = safe_int(pull_request_id)
503 pull_request = PullRequest.get_or_404(pull_request_id)
503 pull_request = PullRequest.get_or_404(pull_request_id)
504 user = c.rhodecode_user
504 user = c.rhodecode_user
505
505
506 check = MergeCheck.validate(pull_request, user)
506 check = MergeCheck.validate(pull_request, user)
507 merge_possible = not check.failed
507 merge_possible = not check.failed
508
508
509 for err_type, error_msg in check.errors:
509 for err_type, error_msg in check.errors:
510 h.flash(error_msg, category=err_type)
510 h.flash(error_msg, category=err_type)
511
511
512 if merge_possible:
512 if merge_possible:
513 log.debug("Pre-conditions checked, trying to merge.")
513 log.debug("Pre-conditions checked, trying to merge.")
514 extras = vcs_operation_context(
514 extras = vcs_operation_context(
515 request.environ, repo_name=pull_request.target_repo.repo_name,
515 request.environ, repo_name=pull_request.target_repo.repo_name,
516 username=user.username, action='push',
516 username=user.username, action='push',
517 scm=pull_request.target_repo.repo_type)
517 scm=pull_request.target_repo.repo_type)
518 self._merge_pull_request(pull_request, user, extras)
518 self._merge_pull_request(pull_request, user, extras)
519
519
520 return redirect(url(
520 return redirect(url(
521 'pullrequest_show',
521 'pullrequest_show',
522 repo_name=pull_request.target_repo.repo_name,
522 repo_name=pull_request.target_repo.repo_name,
523 pull_request_id=pull_request.pull_request_id))
523 pull_request_id=pull_request.pull_request_id))
524
524
525 def _merge_pull_request(self, pull_request, user, extras):
525 def _merge_pull_request(self, pull_request, user, extras):
526 merge_resp = PullRequestModel().merge(
526 merge_resp = PullRequestModel().merge(
527 pull_request, user, extras=extras)
527 pull_request, user, extras=extras)
528
528
529 if merge_resp.executed:
529 if merge_resp.executed:
530 log.debug("The merge was successful, closing the pull request.")
530 log.debug("The merge was successful, closing the pull request.")
531 PullRequestModel().close_pull_request(
531 PullRequestModel().close_pull_request(
532 pull_request.pull_request_id, user)
532 pull_request.pull_request_id, user)
533 Session().commit()
533 Session().commit()
534 msg = _('Pull request was successfully merged and closed.')
534 msg = _('Pull request was successfully merged and closed.')
535 h.flash(msg, category='success')
535 h.flash(msg, category='success')
536 else:
536 else:
537 log.debug(
537 log.debug(
538 "The merge was not successful. Merge response: %s",
538 "The merge was not successful. Merge response: %s",
539 merge_resp)
539 merge_resp)
540 msg = PullRequestModel().merge_status_message(
540 msg = PullRequestModel().merge_status_message(
541 merge_resp.failure_reason)
541 merge_resp.failure_reason)
542 h.flash(msg, category='error')
542 h.flash(msg, category='error')
543
543
544 def _update_reviewers(self, pull_request_id, review_members):
544 def _update_reviewers(self, pull_request_id, review_members):
545 reviewers = [
545 reviewers = [
546 (int(r['user_id']), r['reasons']) for r in review_members]
546 (int(r['user_id']), r['reasons']) for r in review_members]
547 PullRequestModel().update_reviewers(pull_request_id, reviewers)
547 PullRequestModel().update_reviewers(pull_request_id, reviewers)
548 Session().commit()
548 Session().commit()
549
549
550 def _reject_close(self, pull_request):
550 def _reject_close(self, pull_request):
551 if pull_request.is_closed():
551 if pull_request.is_closed():
552 raise HTTPForbidden()
552 raise HTTPForbidden()
553
553
554 PullRequestModel().close_pull_request_with_comment(
554 PullRequestModel().close_pull_request_with_comment(
555 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
555 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
556 Session().commit()
556 Session().commit()
557
557
558 @LoginRequired()
558 @LoginRequired()
559 @NotAnonymous()
559 @NotAnonymous()
560 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
560 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
561 'repository.admin')
561 'repository.admin')
562 @auth.CSRFRequired()
562 @auth.CSRFRequired()
563 @jsonify
563 @jsonify
564 def delete(self, repo_name, pull_request_id):
564 def delete(self, repo_name, pull_request_id):
565 pull_request_id = safe_int(pull_request_id)
565 pull_request_id = safe_int(pull_request_id)
566 pull_request = PullRequest.get_or_404(pull_request_id)
566 pull_request = PullRequest.get_or_404(pull_request_id)
567 # only owner can delete it !
567 # only owner can delete it !
568 if pull_request.author.user_id == c.rhodecode_user.user_id:
568 if pull_request.author.user_id == c.rhodecode_user.user_id:
569 PullRequestModel().delete(pull_request)
569 PullRequestModel().delete(pull_request)
570 Session().commit()
570 Session().commit()
571 h.flash(_('Successfully deleted pull request'),
571 h.flash(_('Successfully deleted pull request'),
572 category='success')
572 category='success')
573 return redirect(url('my_account_pullrequests'))
573 return redirect(url('my_account_pullrequests'))
574 raise HTTPForbidden()
574 raise HTTPForbidden()
575
575
576 def _get_pr_version(self, pull_request_id, version=None):
576 def _get_pr_version(self, pull_request_id, version=None):
577 pull_request_id = safe_int(pull_request_id)
577 pull_request_id = safe_int(pull_request_id)
578 at_version = None
578 at_version = None
579
579
580 if version and version == 'latest':
580 if version and version == 'latest':
581 pull_request_ver = PullRequest.get(pull_request_id)
581 pull_request_ver = PullRequest.get(pull_request_id)
582 pull_request_obj = pull_request_ver
582 pull_request_obj = pull_request_ver
583 _org_pull_request_obj = pull_request_obj
583 _org_pull_request_obj = pull_request_obj
584 at_version = 'latest'
584 at_version = 'latest'
585 elif version:
585 elif version:
586 pull_request_ver = PullRequestVersion.get_or_404(version)
586 pull_request_ver = PullRequestVersion.get_or_404(version)
587 pull_request_obj = pull_request_ver
587 pull_request_obj = pull_request_ver
588 _org_pull_request_obj = pull_request_ver.pull_request
588 _org_pull_request_obj = pull_request_ver.pull_request
589 at_version = pull_request_ver.pull_request_version_id
589 at_version = pull_request_ver.pull_request_version_id
590 else:
590 else:
591 _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)
592
592
593 pull_request_display_obj = PullRequest.get_pr_display_object(
593 pull_request_display_obj = PullRequest.get_pr_display_object(
594 pull_request_obj, _org_pull_request_obj)
594 pull_request_obj, _org_pull_request_obj)
595
595
596 return _org_pull_request_obj, pull_request_obj, \
596 return _org_pull_request_obj, pull_request_obj, \
597 pull_request_display_obj, at_version
597 pull_request_display_obj, at_version
598
598
599 def _get_diffset(
599 def _get_diffset(
600 self, source_repo, source_ref_id, target_ref_id, target_commit,
600 self, source_repo, source_ref_id, target_ref_id, target_commit,
601 source_commit, diff_limit, file_limit, display_inline_comments):
601 source_commit, diff_limit, file_limit, display_inline_comments):
602 vcs_diff = PullRequestModel().get_diff(
602 vcs_diff = PullRequestModel().get_diff(
603 source_repo, source_ref_id, target_ref_id)
603 source_repo, source_ref_id, target_ref_id)
604
604
605 diff_processor = diffs.DiffProcessor(
605 diff_processor = diffs.DiffProcessor(
606 vcs_diff, format='newdiff', diff_limit=diff_limit,
606 vcs_diff, format='newdiff', diff_limit=diff_limit,
607 file_limit=file_limit, show_full_diff=c.fulldiff)
607 file_limit=file_limit, show_full_diff=c.fulldiff)
608
608
609 _parsed = diff_processor.prepare()
609 _parsed = diff_processor.prepare()
610
610
611 def _node_getter(commit):
611 def _node_getter(commit):
612 def get_node(fname):
612 def get_node(fname):
613 try:
613 try:
614 return commit.get_node(fname)
614 return commit.get_node(fname)
615 except NodeDoesNotExistError:
615 except NodeDoesNotExistError:
616 return None
616 return None
617
617
618 return get_node
618 return get_node
619
619
620 diffset = codeblocks.DiffSet(
620 diffset = codeblocks.DiffSet(
621 repo_name=c.repo_name,
621 repo_name=c.repo_name,
622 source_repo_name=c.source_repo.repo_name,
622 source_repo_name=c.source_repo.repo_name,
623 source_node_getter=_node_getter(target_commit),
623 source_node_getter=_node_getter(target_commit),
624 target_node_getter=_node_getter(source_commit),
624 target_node_getter=_node_getter(source_commit),
625 comments=display_inline_comments
625 comments=display_inline_comments
626 )
626 )
627 diffset = diffset.render_patchset(
627 diffset = diffset.render_patchset(
628 _parsed, target_commit.raw_id, source_commit.raw_id)
628 _parsed, target_commit.raw_id, source_commit.raw_id)
629
629
630 return diffset
630 return diffset
631
631
632 @LoginRequired()
632 @LoginRequired()
633 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
633 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
634 'repository.admin')
634 'repository.admin')
635 def show(self, repo_name, pull_request_id):
635 def show(self, repo_name, pull_request_id):
636 pull_request_id = safe_int(pull_request_id)
636 pull_request_id = safe_int(pull_request_id)
637 version = request.GET.get('version')
637 version = request.GET.get('version')
638 from_version = request.GET.get('from_version') or version
638 from_version = request.GET.get('from_version') or version
639 merge_checks = request.GET.get('merge_checks')
639 merge_checks = request.GET.get('merge_checks')
640 c.fulldiff = str2bool(request.GET.get('fulldiff'))
640 c.fulldiff = str2bool(request.GET.get('fulldiff'))
641
641
642 (pull_request_latest,
642 (pull_request_latest,
643 pull_request_at_ver,
643 pull_request_at_ver,
644 pull_request_display_obj,
644 pull_request_display_obj,
645 at_version) = self._get_pr_version(
645 at_version) = self._get_pr_version(
646 pull_request_id, version=version)
646 pull_request_id, version=version)
647 pr_closed = pull_request_latest.is_closed()
647 pr_closed = pull_request_latest.is_closed()
648
648
649 if pr_closed and (version or from_version):
649 if pr_closed and (version or from_version):
650 # not allow to browse versions
650 # not allow to browse versions
651 return redirect(h.url('pullrequest_show', repo_name=repo_name,
651 return redirect(h.url('pullrequest_show', repo_name=repo_name,
652 pull_request_id=pull_request_id))
652 pull_request_id=pull_request_id))
653
653
654 versions = pull_request_display_obj.versions()
654 versions = pull_request_display_obj.versions()
655
655
656 c.at_version = at_version
656 c.at_version = at_version
657 c.at_version_num = (at_version
657 c.at_version_num = (at_version
658 if at_version and at_version != 'latest'
658 if at_version and at_version != 'latest'
659 else None)
659 else None)
660 c.at_version_pos = ChangesetComment.get_index_from_version(
660 c.at_version_pos = ChangesetComment.get_index_from_version(
661 c.at_version_num, versions)
661 c.at_version_num, versions)
662
662
663 (prev_pull_request_latest,
663 (prev_pull_request_latest,
664 prev_pull_request_at_ver,
664 prev_pull_request_at_ver,
665 prev_pull_request_display_obj,
665 prev_pull_request_display_obj,
666 prev_at_version) = self._get_pr_version(
666 prev_at_version) = self._get_pr_version(
667 pull_request_id, version=from_version)
667 pull_request_id, version=from_version)
668
668
669 c.from_version = prev_at_version
669 c.from_version = prev_at_version
670 c.from_version_num = (prev_at_version
670 c.from_version_num = (prev_at_version
671 if prev_at_version and prev_at_version != 'latest'
671 if prev_at_version and prev_at_version != 'latest'
672 else None)
672 else None)
673 c.from_version_pos = ChangesetComment.get_index_from_version(
673 c.from_version_pos = ChangesetComment.get_index_from_version(
674 c.from_version_num, versions)
674 c.from_version_num, versions)
675
675
676 # define if we're in COMPARE mode or VIEW at version mode
676 # define if we're in COMPARE mode or VIEW at version mode
677 compare = at_version != prev_at_version
677 compare = at_version != prev_at_version
678
678
679 # pull_requests repo_name we opened it against
679 # pull_requests repo_name we opened it against
680 # ie. target_repo must match
680 # ie. target_repo must match
681 if repo_name != pull_request_at_ver.target_repo.repo_name:
681 if repo_name != pull_request_at_ver.target_repo.repo_name:
682 raise HTTPNotFound
682 raise HTTPNotFound
683
683
684 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
684 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
685 pull_request_at_ver)
685 pull_request_at_ver)
686
686
687 c.ancestor = None # empty ancestor hidden in display
688 c.pull_request = pull_request_display_obj
687 c.pull_request = pull_request_display_obj
689 c.pull_request_latest = pull_request_latest
688 c.pull_request_latest = pull_request_latest
690
689
691 if compare or (at_version and not at_version == 'latest'):
690 if compare or (at_version and not at_version == 'latest'):
692 c.allowed_to_change_status = False
691 c.allowed_to_change_status = False
693 c.allowed_to_update = False
692 c.allowed_to_update = False
694 c.allowed_to_merge = False
693 c.allowed_to_merge = False
695 c.allowed_to_delete = False
694 c.allowed_to_delete = False
696 c.allowed_to_comment = False
695 c.allowed_to_comment = False
697 c.allowed_to_close = False
696 c.allowed_to_close = False
698 else:
697 else:
699 c.allowed_to_change_status = PullRequestModel(). \
698 c.allowed_to_change_status = PullRequestModel(). \
700 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
699 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
701 and not pr_closed
700 and not pr_closed
702
701
703 c.allowed_to_update = PullRequestModel().check_user_update(
702 c.allowed_to_update = PullRequestModel().check_user_update(
704 pull_request_latest, c.rhodecode_user) and not pr_closed
703 pull_request_latest, c.rhodecode_user) and not pr_closed
705 c.allowed_to_merge = PullRequestModel().check_user_merge(
704 c.allowed_to_merge = PullRequestModel().check_user_merge(
706 pull_request_latest, c.rhodecode_user) and not pr_closed
705 pull_request_latest, c.rhodecode_user) and not pr_closed
707 c.allowed_to_delete = PullRequestModel().check_user_delete(
706 c.allowed_to_delete = PullRequestModel().check_user_delete(
708 pull_request_latest, c.rhodecode_user) and not pr_closed
707 pull_request_latest, c.rhodecode_user) and not pr_closed
709 c.allowed_to_comment = not pr_closed
708 c.allowed_to_comment = not pr_closed
710 c.allowed_to_close = c.allowed_to_change_status and not pr_closed
709 c.allowed_to_close = c.allowed_to_change_status and not pr_closed
711
710
712 # check merge capabilities
711 # check merge capabilities
713 _merge_check = MergeCheck.validate(
712 _merge_check = MergeCheck.validate(
714 pull_request_latest, user=c.rhodecode_user)
713 pull_request_latest, user=c.rhodecode_user)
715 c.pr_merge_errors = _merge_check.error_details
714 c.pr_merge_errors = _merge_check.error_details
716 c.pr_merge_possible = not _merge_check.failed
715 c.pr_merge_possible = not _merge_check.failed
717 c.pr_merge_message = _merge_check.merge_msg
716 c.pr_merge_message = _merge_check.merge_msg
718
717
719 c.pull_request_review_status = _merge_check.review_status
718 c.pull_request_review_status = _merge_check.review_status
720 if merge_checks:
719 if merge_checks:
721 return render('/pullrequests/pullrequest_merge_checks.mako')
720 return render('/pullrequests/pullrequest_merge_checks.mako')
722
721
723 comments_model = CommentsModel()
722 comments_model = CommentsModel()
724
723
725 # reviewers and statuses
724 # reviewers and statuses
726 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
725 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
727 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
726 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
728
727
729 # GENERAL COMMENTS with versions #
728 # GENERAL COMMENTS with versions #
730 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
729 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
731 q = q.order_by(ChangesetComment.comment_id.asc())
730 q = q.order_by(ChangesetComment.comment_id.asc())
732 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
731 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
733
732
734 # pick comments we want to render at current version
733 # pick comments we want to render at current version
735 c.comment_versions = comments_model.aggregate_comments(
734 c.comment_versions = comments_model.aggregate_comments(
736 general_comments, versions, c.at_version_num)
735 general_comments, versions, c.at_version_num)
737 c.comments = c.comment_versions[c.at_version_num]['until']
736 c.comments = c.comment_versions[c.at_version_num]['until']
738
737
739 # INLINE COMMENTS with versions #
738 # INLINE COMMENTS with versions #
740 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
739 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
741 q = q.order_by(ChangesetComment.comment_id.asc())
740 q = q.order_by(ChangesetComment.comment_id.asc())
742 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
741 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
743 c.inline_versions = comments_model.aggregate_comments(
742 c.inline_versions = comments_model.aggregate_comments(
744 inline_comments, versions, c.at_version_num, inline=True)
743 inline_comments, versions, c.at_version_num, inline=True)
745
744
746 # inject latest version
745 # inject latest version
747 latest_ver = PullRequest.get_pr_display_object(
746 latest_ver = PullRequest.get_pr_display_object(
748 pull_request_latest, pull_request_latest)
747 pull_request_latest, pull_request_latest)
749
748
750 c.versions = versions + [latest_ver]
749 c.versions = versions + [latest_ver]
751
750
752 # if we use version, then do not show later comments
751 # if we use version, then do not show later comments
753 # than current version
752 # than current version
754 display_inline_comments = collections.defaultdict(
753 display_inline_comments = collections.defaultdict(
755 lambda: collections.defaultdict(list))
754 lambda: collections.defaultdict(list))
756 for co in inline_comments:
755 for co in inline_comments:
757 if c.at_version_num:
756 if c.at_version_num:
758 # pick comments that are at least UPTO given version, so we
757 # pick comments that are at least UPTO given version, so we
759 # don't render comments for higher version
758 # don't render comments for higher version
760 should_render = co.pull_request_version_id and \
759 should_render = co.pull_request_version_id and \
761 co.pull_request_version_id <= c.at_version_num
760 co.pull_request_version_id <= c.at_version_num
762 else:
761 else:
763 # showing all, for 'latest'
762 # showing all, for 'latest'
764 should_render = True
763 should_render = True
765
764
766 if should_render:
765 if should_render:
767 display_inline_comments[co.f_path][co.line_no].append(co)
766 display_inline_comments[co.f_path][co.line_no].append(co)
768
767
769 # load diff data into template context, if we use compare mode then
768 # load diff data into template context, if we use compare mode then
770 # diff is calculated based on changes between versions of PR
769 # diff is calculated based on changes between versions of PR
771
770
772 source_repo = pull_request_at_ver.source_repo
771 source_repo = pull_request_at_ver.source_repo
773 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
772 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
774
773
775 target_repo = pull_request_at_ver.target_repo
774 target_repo = pull_request_at_ver.target_repo
776 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
775 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
777
776
778 if compare:
777 if compare:
779 # in compare switch the diff base to latest commit from prev version
778 # in compare switch the diff base to latest commit from prev version
780 target_ref_id = prev_pull_request_display_obj.revisions[0]
779 target_ref_id = prev_pull_request_display_obj.revisions[0]
781
780
782 # despite opening commits for bookmarks/branches/tags, we always
781 # despite opening commits for bookmarks/branches/tags, we always
783 # convert this to rev to prevent changes after bookmark or branch change
782 # convert this to rev to prevent changes after bookmark or branch change
784 c.source_ref_type = 'rev'
783 c.source_ref_type = 'rev'
785 c.source_ref = source_ref_id
784 c.source_ref = source_ref_id
786
785
787 c.target_ref_type = 'rev'
786 c.target_ref_type = 'rev'
788 c.target_ref = target_ref_id
787 c.target_ref = target_ref_id
789
788
790 c.source_repo = source_repo
789 c.source_repo = source_repo
791 c.target_repo = target_repo
790 c.target_repo = target_repo
792
791
793 # diff_limit is the old behavior, will cut off the whole diff
792 # diff_limit is the old behavior, will cut off the whole diff
794 # if the limit is applied otherwise will just hide the
793 # if the limit is applied otherwise will just hide the
795 # big files from the front-end
794 # big files from the front-end
796 diff_limit = self.cut_off_limit_diff
795 diff_limit = self.cut_off_limit_diff
797 file_limit = self.cut_off_limit_file
796 file_limit = self.cut_off_limit_file
798
797
799 c.commit_ranges = []
798 c.commit_ranges = []
800 source_commit = EmptyCommit()
799 source_commit = EmptyCommit()
801 target_commit = EmptyCommit()
800 target_commit = EmptyCommit()
802 c.missing_requirements = False
801 c.missing_requirements = False
803
802
803 source_scm = source_repo.scm_instance()
804 target_scm = target_repo.scm_instance()
805
804 # try first shadow repo, fallback to regular repo
806 # try first shadow repo, fallback to regular repo
805 try:
807 try:
806 commits_source_repo = pull_request_latest.get_shadow_repo()
808 commits_source_repo = pull_request_latest.get_shadow_repo()
807 except Exception:
809 except Exception:
808 log.debug('Failed to get shadow repo', exc_info=True)
810 log.debug('Failed to get shadow repo', exc_info=True)
809 commits_source_repo = source_repo.scm_instance()
811 commits_source_repo = source_scm
810
812
811 c.commits_source_repo = commits_source_repo
813 c.commits_source_repo = commits_source_repo
812 commit_cache = {}
814 commit_cache = {}
813 try:
815 try:
814 pre_load = ["author", "branch", "date", "message"]
816 pre_load = ["author", "branch", "date", "message"]
815 show_revs = pull_request_at_ver.revisions
817 show_revs = pull_request_at_ver.revisions
816 for rev in show_revs:
818 for rev in show_revs:
817 comm = commits_source_repo.get_commit(
819 comm = commits_source_repo.get_commit(
818 commit_id=rev, pre_load=pre_load)
820 commit_id=rev, pre_load=pre_load)
819 c.commit_ranges.append(comm)
821 c.commit_ranges.append(comm)
820 commit_cache[comm.raw_id] = comm
822 commit_cache[comm.raw_id] = comm
821
823
822 target_commit = commits_source_repo.get_commit(
824 target_commit = commits_source_repo.get_commit(
823 commit_id=safe_str(target_ref_id))
825 commit_id=safe_str(target_ref_id))
824 source_commit = commits_source_repo.get_commit(
826 source_commit = commits_source_repo.get_commit(
825 commit_id=safe_str(source_ref_id))
827 commit_id=safe_str(source_ref_id))
826 except CommitDoesNotExistError:
828 except CommitDoesNotExistError:
827 pass
829 pass
828 except RepositoryRequirementError:
830 except RepositoryRequirementError:
829 log.warning(
831 log.warning(
830 'Failed to get all required data from repo', exc_info=True)
832 'Failed to get all required data from repo', exc_info=True)
831 c.missing_requirements = True
833 c.missing_requirements = True
832
834
835 c.ancestor = None # set it to None, to hide it from PR view
836
837 try:
838 ancestor_id = source_scm.get_common_ancestor(
839 source_commit.raw_id, target_commit.raw_id, target_scm)
840 c.ancestor_commit = source_scm.get_commit(ancestor_id)
841 except Exception:
842 c.ancestor_commit = None
843
833 c.statuses = source_repo.statuses(
844 c.statuses = source_repo.statuses(
834 [x.raw_id for x in c.commit_ranges])
845 [x.raw_id for x in c.commit_ranges])
835
846
836 # auto collapse if we have more than limit
847 # auto collapse if we have more than limit
837 collapse_limit = diffs.DiffProcessor._collapse_commits_over
848 collapse_limit = diffs.DiffProcessor._collapse_commits_over
838 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
849 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
839 c.compare_mode = compare
850 c.compare_mode = compare
840
851
841 c.missing_commits = False
852 c.missing_commits = False
842 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
853 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
843 or source_commit == target_commit):
854 or source_commit == target_commit):
844
855
845 c.missing_commits = True
856 c.missing_commits = True
846 else:
857 else:
847
858
848 c.diffset = self._get_diffset(
859 c.diffset = self._get_diffset(
849 commits_source_repo, source_ref_id, target_ref_id,
860 commits_source_repo, source_ref_id, target_ref_id,
850 target_commit, source_commit,
861 target_commit, source_commit,
851 diff_limit, file_limit, display_inline_comments)
862 diff_limit, file_limit, display_inline_comments)
852
863
853 c.limited_diff = c.diffset.limited_diff
864 c.limited_diff = c.diffset.limited_diff
854
865
855 # calculate removed files that are bound to comments
866 # calculate removed files that are bound to comments
856 comment_deleted_files = [
867 comment_deleted_files = [
857 fname for fname in display_inline_comments
868 fname for fname in display_inline_comments
858 if fname not in c.diffset.file_stats]
869 if fname not in c.diffset.file_stats]
859
870
860 c.deleted_files_comments = collections.defaultdict(dict)
871 c.deleted_files_comments = collections.defaultdict(dict)
861 for fname, per_line_comments in display_inline_comments.items():
872 for fname, per_line_comments in display_inline_comments.items():
862 if fname in comment_deleted_files:
873 if fname in comment_deleted_files:
863 c.deleted_files_comments[fname]['stats'] = 0
874 c.deleted_files_comments[fname]['stats'] = 0
864 c.deleted_files_comments[fname]['comments'] = list()
875 c.deleted_files_comments[fname]['comments'] = list()
865 for lno, comments in per_line_comments.items():
876 for lno, comments in per_line_comments.items():
866 c.deleted_files_comments[fname]['comments'].extend(
877 c.deleted_files_comments[fname]['comments'].extend(
867 comments)
878 comments)
868
879
869 # this is a hack to properly display links, when creating PR, the
880 # this is a hack to properly display links, when creating PR, the
870 # compare view and others uses different notation, and
881 # compare view and others uses different notation, and
871 # compare_commits.mako renders links based on the target_repo.
882 # compare_commits.mako renders links based on the target_repo.
872 # We need to swap that here to generate it properly on the html side
883 # We need to swap that here to generate it properly on the html side
873 c.target_repo = c.source_repo
884 c.target_repo = c.source_repo
874
885
875 c.commit_statuses = ChangesetStatus.STATUSES
886 c.commit_statuses = ChangesetStatus.STATUSES
876
887
877 c.show_version_changes = not pr_closed
888 c.show_version_changes = not pr_closed
878 if c.show_version_changes:
889 if c.show_version_changes:
879 cur_obj = pull_request_at_ver
890 cur_obj = pull_request_at_ver
880 prev_obj = prev_pull_request_at_ver
891 prev_obj = prev_pull_request_at_ver
881
892
882 old_commit_ids = prev_obj.revisions
893 old_commit_ids = prev_obj.revisions
883 new_commit_ids = cur_obj.revisions
894 new_commit_ids = cur_obj.revisions
884 commit_changes = PullRequestModel()._calculate_commit_id_changes(
895 commit_changes = PullRequestModel()._calculate_commit_id_changes(
885 old_commit_ids, new_commit_ids)
896 old_commit_ids, new_commit_ids)
886 c.commit_changes_summary = commit_changes
897 c.commit_changes_summary = commit_changes
887
898
888 # calculate the diff for commits between versions
899 # calculate the diff for commits between versions
889 c.commit_changes = []
900 c.commit_changes = []
890 mark = lambda cs, fw: list(
901 mark = lambda cs, fw: list(
891 h.itertools.izip_longest([], cs, fillvalue=fw))
902 h.itertools.izip_longest([], cs, fillvalue=fw))
892 for c_type, raw_id in mark(commit_changes.added, 'a') \
903 for c_type, raw_id in mark(commit_changes.added, 'a') \
893 + mark(commit_changes.removed, 'r') \
904 + mark(commit_changes.removed, 'r') \
894 + mark(commit_changes.common, 'c'):
905 + mark(commit_changes.common, 'c'):
895
906
896 if raw_id in commit_cache:
907 if raw_id in commit_cache:
897 commit = commit_cache[raw_id]
908 commit = commit_cache[raw_id]
898 else:
909 else:
899 try:
910 try:
900 commit = commits_source_repo.get_commit(raw_id)
911 commit = commits_source_repo.get_commit(raw_id)
901 except CommitDoesNotExistError:
912 except CommitDoesNotExistError:
902 # in case we fail extracting still use "dummy" commit
913 # in case we fail extracting still use "dummy" commit
903 # for display in commit diff
914 # for display in commit diff
904 commit = h.AttributeDict(
915 commit = h.AttributeDict(
905 {'raw_id': raw_id,
916 {'raw_id': raw_id,
906 'message': 'EMPTY or MISSING COMMIT'})
917 'message': 'EMPTY or MISSING COMMIT'})
907 c.commit_changes.append([c_type, commit])
918 c.commit_changes.append([c_type, commit])
908
919
909 # current user review statuses for each version
920 # current user review statuses for each version
910 c.review_versions = {}
921 c.review_versions = {}
911 if c.rhodecode_user.user_id in allowed_reviewers:
922 if c.rhodecode_user.user_id in allowed_reviewers:
912 for co in general_comments:
923 for co in general_comments:
913 if co.author.user_id == c.rhodecode_user.user_id:
924 if co.author.user_id == c.rhodecode_user.user_id:
914 # each comment has a status change
925 # each comment has a status change
915 status = co.status_change
926 status = co.status_change
916 if status:
927 if status:
917 _ver_pr = status[0].comment.pull_request_version_id
928 _ver_pr = status[0].comment.pull_request_version_id
918 c.review_versions[_ver_pr] = status[0]
929 c.review_versions[_ver_pr] = status[0]
919
930
920 return render('/pullrequests/pullrequest_show.mako')
931 return render('/pullrequests/pullrequest_show.mako')
921
932
922 @LoginRequired()
933 @LoginRequired()
923 @NotAnonymous()
934 @NotAnonymous()
924 @HasRepoPermissionAnyDecorator(
935 @HasRepoPermissionAnyDecorator(
925 'repository.read', 'repository.write', 'repository.admin')
936 'repository.read', 'repository.write', 'repository.admin')
926 @auth.CSRFRequired()
937 @auth.CSRFRequired()
927 @jsonify
938 @jsonify
928 def comment(self, repo_name, pull_request_id):
939 def comment(self, repo_name, pull_request_id):
929 pull_request_id = safe_int(pull_request_id)
940 pull_request_id = safe_int(pull_request_id)
930 pull_request = PullRequest.get_or_404(pull_request_id)
941 pull_request = PullRequest.get_or_404(pull_request_id)
931 if pull_request.is_closed():
942 if pull_request.is_closed():
932 raise HTTPForbidden()
943 raise HTTPForbidden()
933
944
934 status = request.POST.get('changeset_status', None)
945 status = request.POST.get('changeset_status', None)
935 text = request.POST.get('text')
946 text = request.POST.get('text')
936 comment_type = request.POST.get('comment_type')
947 comment_type = request.POST.get('comment_type')
937 resolves_comment_id = request.POST.get('resolves_comment_id', None)
948 resolves_comment_id = request.POST.get('resolves_comment_id', None)
938 close_pull_request = request.POST.get('close_pull_request')
949 close_pull_request = request.POST.get('close_pull_request')
939
950
940 close_pr = False
951 close_pr = False
941 if close_pull_request:
952 if close_pull_request:
942 close_pr = True
953 close_pr = True
943 pull_request_review_status = pull_request.calculated_review_status()
954 pull_request_review_status = pull_request.calculated_review_status()
944 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
955 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
945 # approved only if we have voting consent
956 # approved only if we have voting consent
946 status = ChangesetStatus.STATUS_APPROVED
957 status = ChangesetStatus.STATUS_APPROVED
947 else:
958 else:
948 status = ChangesetStatus.STATUS_REJECTED
959 status = ChangesetStatus.STATUS_REJECTED
949
960
950 allowed_to_change_status = PullRequestModel().check_user_change_status(
961 allowed_to_change_status = PullRequestModel().check_user_change_status(
951 pull_request, c.rhodecode_user)
962 pull_request, c.rhodecode_user)
952
963
953 if status and allowed_to_change_status:
964 if status and allowed_to_change_status:
954 message = (_('Status change %(transition_icon)s %(status)s')
965 message = (_('Status change %(transition_icon)s %(status)s')
955 % {'transition_icon': '>',
966 % {'transition_icon': '>',
956 'status': ChangesetStatus.get_status_lbl(status)})
967 'status': ChangesetStatus.get_status_lbl(status)})
957 if close_pr:
968 if close_pr:
958 message = _('Closing with') + ' ' + message
969 message = _('Closing with') + ' ' + message
959 text = text or message
970 text = text or message
960 comm = CommentsModel().create(
971 comm = CommentsModel().create(
961 text=text,
972 text=text,
962 repo=c.rhodecode_db_repo.repo_id,
973 repo=c.rhodecode_db_repo.repo_id,
963 user=c.rhodecode_user.user_id,
974 user=c.rhodecode_user.user_id,
964 pull_request=pull_request_id,
975 pull_request=pull_request_id,
965 f_path=request.POST.get('f_path'),
976 f_path=request.POST.get('f_path'),
966 line_no=request.POST.get('line'),
977 line_no=request.POST.get('line'),
967 status_change=(ChangesetStatus.get_status_lbl(status)
978 status_change=(ChangesetStatus.get_status_lbl(status)
968 if status and allowed_to_change_status else None),
979 if status and allowed_to_change_status else None),
969 status_change_type=(status
980 status_change_type=(status
970 if status and allowed_to_change_status else None),
981 if status and allowed_to_change_status else None),
971 closing_pr=close_pr,
982 closing_pr=close_pr,
972 comment_type=comment_type,
983 comment_type=comment_type,
973 resolves_comment_id=resolves_comment_id
984 resolves_comment_id=resolves_comment_id
974 )
985 )
975
986
976 if allowed_to_change_status:
987 if allowed_to_change_status:
977 old_calculated_status = pull_request.calculated_review_status()
988 old_calculated_status = pull_request.calculated_review_status()
978 # get status if set !
989 # get status if set !
979 if status:
990 if status:
980 ChangesetStatusModel().set_status(
991 ChangesetStatusModel().set_status(
981 c.rhodecode_db_repo.repo_id,
992 c.rhodecode_db_repo.repo_id,
982 status,
993 status,
983 c.rhodecode_user.user_id,
994 c.rhodecode_user.user_id,
984 comm,
995 comm,
985 pull_request=pull_request_id
996 pull_request=pull_request_id
986 )
997 )
987
998
988 Session().flush()
999 Session().flush()
989 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
1000 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
990 # we now calculate the status of pull request, and based on that
1001 # we now calculate the status of pull request, and based on that
991 # calculation we set the commits status
1002 # calculation we set the commits status
992 calculated_status = pull_request.calculated_review_status()
1003 calculated_status = pull_request.calculated_review_status()
993 if old_calculated_status != calculated_status:
1004 if old_calculated_status != calculated_status:
994 PullRequestModel()._trigger_pull_request_hook(
1005 PullRequestModel()._trigger_pull_request_hook(
995 pull_request, c.rhodecode_user, 'review_status_change')
1006 pull_request, c.rhodecode_user, 'review_status_change')
996
1007
997 calculated_status_lbl = ChangesetStatus.get_status_lbl(
1008 calculated_status_lbl = ChangesetStatus.get_status_lbl(
998 calculated_status)
1009 calculated_status)
999
1010
1000 if close_pr:
1011 if close_pr:
1001 status_completed = (
1012 status_completed = (
1002 calculated_status in [ChangesetStatus.STATUS_APPROVED,
1013 calculated_status in [ChangesetStatus.STATUS_APPROVED,
1003 ChangesetStatus.STATUS_REJECTED])
1014 ChangesetStatus.STATUS_REJECTED])
1004 if close_pull_request or status_completed:
1015 if close_pull_request or status_completed:
1005 PullRequestModel().close_pull_request(
1016 PullRequestModel().close_pull_request(
1006 pull_request_id, c.rhodecode_user)
1017 pull_request_id, c.rhodecode_user)
1007 else:
1018 else:
1008 h.flash(_('Closing pull request on other statuses than '
1019 h.flash(_('Closing pull request on other statuses than '
1009 'rejected or approved is forbidden. '
1020 'rejected or approved is forbidden. '
1010 'Calculated status from all reviewers '
1021 'Calculated status from all reviewers '
1011 'is currently: %s') % calculated_status_lbl,
1022 'is currently: %s') % calculated_status_lbl,
1012 category='warning')
1023 category='warning')
1013
1024
1014 Session().commit()
1025 Session().commit()
1015
1026
1016 if not request.is_xhr:
1027 if not request.is_xhr:
1017 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1028 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1018 pull_request_id=pull_request_id))
1029 pull_request_id=pull_request_id))
1019
1030
1020 data = {
1031 data = {
1021 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1032 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1022 }
1033 }
1023 if comm:
1034 if comm:
1024 c.co = comm
1035 c.co = comm
1025 c.inline_comment = True if comm.line_no else False
1036 c.inline_comment = True if comm.line_no else False
1026 data.update(comm.get_dict())
1037 data.update(comm.get_dict())
1027 data.update({'rendered_text':
1038 data.update({'rendered_text':
1028 render('changeset/changeset_comment_block.mako')})
1039 render('changeset/changeset_comment_block.mako')})
1029
1040
1030 return data
1041 return data
1031
1042
1032 @LoginRequired()
1043 @LoginRequired()
1033 @NotAnonymous()
1044 @NotAnonymous()
1034 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1045 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1035 'repository.admin')
1046 'repository.admin')
1036 @auth.CSRFRequired()
1047 @auth.CSRFRequired()
1037 @jsonify
1048 @jsonify
1038 def delete_comment(self, repo_name, comment_id):
1049 def delete_comment(self, repo_name, comment_id):
1039 return self._delete_comment(comment_id)
1050 return self._delete_comment(comment_id)
1040
1051
1041 def _delete_comment(self, comment_id):
1052 def _delete_comment(self, comment_id):
1042 comment_id = safe_int(comment_id)
1053 comment_id = safe_int(comment_id)
1043 co = ChangesetComment.get_or_404(comment_id)
1054 co = ChangesetComment.get_or_404(comment_id)
1044 if co.pull_request.is_closed():
1055 if co.pull_request.is_closed():
1045 # don't allow deleting comments on closed pull request
1056 # don't allow deleting comments on closed pull request
1046 raise HTTPForbidden()
1057 raise HTTPForbidden()
1047
1058
1048 is_owner = co.author.user_id == c.rhodecode_user.user_id
1059 is_owner = co.author.user_id == c.rhodecode_user.user_id
1049 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1060 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1050 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1061 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1051 old_calculated_status = co.pull_request.calculated_review_status()
1062 old_calculated_status = co.pull_request.calculated_review_status()
1052 CommentsModel().delete(comment=co)
1063 CommentsModel().delete(comment=co)
1053 Session().commit()
1064 Session().commit()
1054 calculated_status = co.pull_request.calculated_review_status()
1065 calculated_status = co.pull_request.calculated_review_status()
1055 if old_calculated_status != calculated_status:
1066 if old_calculated_status != calculated_status:
1056 PullRequestModel()._trigger_pull_request_hook(
1067 PullRequestModel()._trigger_pull_request_hook(
1057 co.pull_request, c.rhodecode_user, 'review_status_change')
1068 co.pull_request, c.rhodecode_user, 'review_status_change')
1058 return True
1069 return True
1059 else:
1070 else:
1060 raise HTTPForbidden()
1071 raise HTTPForbidden()
@@ -1,821 +1,826 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 <br/>
87 % if c.ancestor_commit:
88 ${_('Common ancestor')}:
89 <code><a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 % endif
86 </div>
91 </div>
87 <div class="pr-pullinfo">
92 <div class="pr-pullinfo">
88 %if h.is_hg(c.pull_request.source_repo):
93 %if h.is_hg(c.pull_request.source_repo):
89 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
94 <input type="text" class="input-monospace" 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):
95 %elif h.is_git(c.pull_request.source_repo):
91 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
92 %endif
97 %endif
93 </div>
98 </div>
94 </div>
99 </div>
95 </div>
100 </div>
96 <div class="field">
101 <div class="field">
97 <div class="label-summary">
102 <div class="label-summary">
98 <label>${_('Target')}:</label>
103 <label>${_('Target')}:</label>
99 </div>
104 </div>
100 <div class="input">
105 <div class="input">
101 <div class="pr-targetinfo">
106 <div class="pr-targetinfo">
102 ## branch link is only valid if it is a branch
107 ## branch link is only valid if it is a branch
103 <span class="tag">
108 <span class="tag">
104 %if c.pull_request.target_ref_parts.type == 'branch':
109 %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>
110 <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:
111 %else:
107 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
108 %endif
113 %endif
109 </span>
114 </span>
110 <span class="clone-url">
115 <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>
116 <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>
117 </span>
113 </div>
118 </div>
114 </div>
119 </div>
115 </div>
120 </div>
116
121
117 ## Link to the shadow repository.
122 ## Link to the shadow repository.
118 <div class="field">
123 <div class="field">
119 <div class="label-summary">
124 <div class="label-summary">
120 <label>${_('Merge')}:</label>
125 <label>${_('Merge')}:</label>
121 </div>
126 </div>
122 <div class="input">
127 <div class="input">
123 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
124 <div class="pr-mergeinfo">
129 <div class="pr-mergeinfo">
125 %if h.is_hg(c.pull_request.target_repo):
130 %if h.is_hg(c.pull_request.target_repo):
126 <input type="text" class="input-monospace" 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">
131 <input type="text" class="input-monospace" 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):
132 %elif h.is_git(c.pull_request.target_repo):
128 <input type="text" class="input-monospace" 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">
133 <input type="text" class="input-monospace" 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
134 %endif
130 </div>
135 </div>
131 % else:
136 % else:
132 <div class="">
137 <div class="">
133 ${_('Shadow repository data not available')}.
138 ${_('Shadow repository data not available')}.
134 </div>
139 </div>
135 % endif
140 % endif
136 </div>
141 </div>
137 </div>
142 </div>
138
143
139 <div class="field">
144 <div class="field">
140 <div class="label-summary">
145 <div class="label-summary">
141 <label>${_('Review')}:</label>
146 <label>${_('Review')}:</label>
142 </div>
147 </div>
143 <div class="input">
148 <div class="input">
144 %if c.pull_request_review_status:
149 %if c.pull_request_review_status:
145 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
146 <span class="changeset-status-lbl tooltip">
151 <span class="changeset-status-lbl tooltip">
147 %if c.pull_request.is_closed():
152 %if c.pull_request.is_closed():
148 ${_('Closed')},
153 ${_('Closed')},
149 %endif
154 %endif
150 ${h.commit_status_lbl(c.pull_request_review_status)}
155 ${h.commit_status_lbl(c.pull_request_review_status)}
151 </span>
156 </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)}
157 - ${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
158 %endif
154 </div>
159 </div>
155 </div>
160 </div>
156 <div class="field">
161 <div class="field">
157 <div class="pr-description-label label-summary">
162 <div class="pr-description-label label-summary">
158 <label>${_('Description')}:</label>
163 <label>${_('Description')}:</label>
159 </div>
164 </div>
160 <div id="pr-desc" class="input">
165 <div id="pr-desc" class="input">
161 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
162 </div>
167 </div>
163 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
168 <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>
169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
165 </div>
170 </div>
166 </div>
171 </div>
167
172
168 <div class="field">
173 <div class="field">
169 <div class="label-summary">
174 <div class="label-summary">
170 <label>${_('Versions')}:</label>
175 <label>${_('Versions')}:</label>
171 </div>
176 </div>
172
177
173 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
174 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
175
180
176 <div class="pr-versions">
181 <div class="pr-versions">
177 % if c.show_version_changes:
182 % if c.show_version_changes:
178 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
183 <% 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']) %>
184 <% 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"
185 <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))}"
186 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')}">
187 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))}
188 ${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>
189 </a>
185 <table>
190 <table>
186 ## SHOW ALL VERSIONS OF PR
191 ## SHOW ALL VERSIONS OF PR
187 <% ver_pr = None %>
192 <% ver_pr = None %>
188
193
189 % for data in reversed(list(enumerate(c.versions, 1))):
194 % for data in reversed(list(enumerate(c.versions, 1))):
190 <% ver_pos = data[0] %>
195 <% ver_pos = data[0] %>
191 <% ver = data[1] %>
196 <% ver = data[1] %>
192 <% ver_pr = ver.pull_request_version_id %>
197 <% 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' %>
198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
194
199
195 <tr class="version-pr" style="display: ${display_row}">
200 <tr class="version-pr" style="display: ${display_row}">
196 <td>
201 <td>
197 <code>
202 <code>
198 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
199 </code>
204 </code>
200 </td>
205 </td>
201 <td>
206 <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}"/>
207 <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}"/>
208 <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>
209 </td>
205 <td>
210 <td>
206 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
211 <% 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')}">
212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
208 </div>
213 </div>
209 </td>
214 </td>
210 <td>
215 <td>
211 % if c.at_version_num != ver_pr:
216 % if c.at_version_num != ver_pr:
212 <i class="icon-comment"></i>
217 <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']))}">
218 <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'])}
219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
215 </code>
220 </code>
216 % endif
221 % endif
217 </td>
222 </td>
218 <td>
223 <td>
219 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
220 </td>
225 </td>
221 <td>
226 <td>
222 ${h.age_component(ver.updated_on, time_is_local=True)}
227 ${h.age_component(ver.updated_on, time_is_local=True)}
223 </td>
228 </td>
224 </tr>
229 </tr>
225 % endfor
230 % endfor
226
231
227 <tr>
232 <tr>
228 <td colspan="6">
233 <td colspan="6">
229 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
234 <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')}"
235 data-label-text-locked="${_('select versions to show changes')}"
231 data-label-text-diff="${_('show changes between versions')}"
236 data-label-text-diff="${_('show changes between versions')}"
232 data-label-text-show="${_('show pull request for this version')}"
237 data-label-text-show="${_('show pull request for this version')}"
233 >
238 >
234 ${_('select versions to show changes')}
239 ${_('select versions to show changes')}
235 </button>
240 </button>
236 </td>
241 </td>
237 </tr>
242 </tr>
238
243
239 ## show comment/inline comments summary
244 ## show comment/inline comments summary
240 <%def name="comments_summary()">
245 <%def name="comments_summary()">
241 <tr>
246 <tr>
242 <td colspan="6" class="comments-summary-td">
247 <td colspan="6" class="comments-summary-td">
243
248
244 % if c.at_version:
249 % if c.at_version:
245 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
250 <% 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']) %>
251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
247 ${_('Comments at this version')}:
252 ${_('Comments at this version')}:
248 % else:
253 % else:
249 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
254 <% 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']) %>
255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
251 ${_('Comments for this pull request')}:
256 ${_('Comments for this pull request')}:
252 % endif
257 % endif
253
258
254
259
255 %if general_comm_count_ver:
260 %if general_comm_count_ver:
256 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
257 %else:
262 %else:
258 ${_("%d General ") % general_comm_count_ver}
263 ${_("%d General ") % general_comm_count_ver}
259 %endif
264 %endif
260
265
261 %if inline_comm_count_ver:
266 %if inline_comm_count_ver:
262 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
263 %else:
268 %else:
264 , ${_("%d Inline") % inline_comm_count_ver}
269 , ${_("%d Inline") % inline_comm_count_ver}
265 %endif
270 %endif
266
271
267 %if outdated_comm_count_ver:
272 %if outdated_comm_count_ver:
268 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
273 , <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>
274 <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>
275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
271 %else:
276 %else:
272 , ${_("%d Outdated") % outdated_comm_count_ver}
277 , ${_("%d Outdated") % outdated_comm_count_ver}
273 %endif
278 %endif
274 </td>
279 </td>
275 </tr>
280 </tr>
276 </%def>
281 </%def>
277 ${comments_summary()}
282 ${comments_summary()}
278 </table>
283 </table>
279 % else:
284 % else:
280 <div class="input">
285 <div class="input">
281 ${_('Pull request versions not available')}.
286 ${_('Pull request versions not available')}.
282 </div>
287 </div>
283 <div>
288 <div>
284 <table>
289 <table>
285 ${comments_summary()}
290 ${comments_summary()}
286 </table>
291 </table>
287 </div>
292 </div>
288 % endif
293 % endif
289 </div>
294 </div>
290 </div>
295 </div>
291
296
292 <div id="pr-save" class="field" style="display: none;">
297 <div id="pr-save" class="field" style="display: none;">
293 <div class="label-summary"></div>
298 <div class="label-summary"></div>
294 <div class="input">
299 <div class="input">
295 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
300 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
296 </div>
301 </div>
297 </div>
302 </div>
298 </div>
303 </div>
299 </div>
304 </div>
300 <div>
305 <div>
301 ## AUTHOR
306 ## AUTHOR
302 <div class="reviewers-title block-right">
307 <div class="reviewers-title block-right">
303 <div class="pr-details-title">
308 <div class="pr-details-title">
304 ${_('Author')}
309 ${_('Author')}
305 </div>
310 </div>
306 </div>
311 </div>
307 <div class="block-right pr-details-content reviewers">
312 <div class="block-right pr-details-content reviewers">
308 <ul class="group_members">
313 <ul class="group_members">
309 <li>
314 <li>
310 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
311 </li>
316 </li>
312 </ul>
317 </ul>
313 </div>
318 </div>
314 ## REVIEWERS
319 ## REVIEWERS
315 <div class="reviewers-title block-right">
320 <div class="reviewers-title block-right">
316 <div class="pr-details-title">
321 <div class="pr-details-title">
317 ${_('Pull request reviewers')}
322 ${_('Pull request reviewers')}
318 %if c.allowed_to_update:
323 %if c.allowed_to_update:
319 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
324 <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>
325 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
321 %endif
326 %endif
322 </div>
327 </div>
323 </div>
328 </div>
324 <div id="reviewers" class="block-right pr-details-content reviewers">
329 <div id="reviewers" class="block-right pr-details-content reviewers">
325 ## members goes here !
330 ## members goes here !
326 <input type="hidden" name="__start__" value="review_members:sequence">
331 <input type="hidden" name="__start__" value="review_members:sequence">
327 <ul id="review_members" class="group_members">
332 <ul id="review_members" class="group_members">
328 %for member,reasons,status in c.pull_request_reviewers:
333 %for member,reasons,status in c.pull_request_reviewers:
329 <li id="reviewer_${member.user_id}">
334 <li id="reviewer_${member.user_id}">
330 <div class="reviewers_member">
335 <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'))}">
336 <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>
337 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
333 </div>
338 </div>
334 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
339 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
335 ${self.gravatar_with_user(member.email, 16)}
340 ${self.gravatar_with_user(member.email, 16)}
336 </div>
341 </div>
337 <input type="hidden" name="__start__" value="reviewer:mapping">
342 <input type="hidden" name="__start__" value="reviewer:mapping">
338 <input type="hidden" name="__start__" value="reasons:sequence">
343 <input type="hidden" name="__start__" value="reasons:sequence">
339 %for reason in reasons:
344 %for reason in reasons:
340 <div class="reviewer_reason">- ${reason}</div>
345 <div class="reviewer_reason">- ${reason}</div>
341 <input type="hidden" name="reason" value="${reason}">
346 <input type="hidden" name="reason" value="${reason}">
342
347
343 %endfor
348 %endfor
344 <input type="hidden" name="__end__" value="reasons:sequence">
349 <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" />
350 <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">
351 <input type="hidden" name="__end__" value="reviewer:mapping">
347 %if c.allowed_to_update:
352 %if c.allowed_to_update:
348 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
353 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
349 <i class="icon-remove-sign" ></i>
354 <i class="icon-remove-sign" ></i>
350 </div>
355 </div>
351 %endif
356 %endif
352 </div>
357 </div>
353 </li>
358 </li>
354 %endfor
359 %endfor
355 </ul>
360 </ul>
356 <input type="hidden" name="__end__" value="review_members:sequence">
361 <input type="hidden" name="__end__" value="review_members:sequence">
357 %if not c.pull_request.is_closed():
362 %if not c.pull_request.is_closed():
358 <div id="add_reviewer_input" class='ac' style="display: none;">
363 <div id="add_reviewer_input" class='ac' style="display: none;">
359 %if c.allowed_to_update:
364 %if c.allowed_to_update:
360 <div class="reviewer_ac">
365 <div class="reviewer_ac">
361 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
366 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
362 <div id="reviewers_container"></div>
367 <div id="reviewers_container"></div>
363 </div>
368 </div>
364 <div>
369 <div>
365 <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button>
370 <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button>
366 </div>
371 </div>
367 %endif
372 %endif
368 </div>
373 </div>
369 %endif
374 %endif
370 </div>
375 </div>
371 </div>
376 </div>
372 </div>
377 </div>
373 <div class="box">
378 <div class="box">
374 ##DIFF
379 ##DIFF
375 <div class="table" >
380 <div class="table" >
376 <div id="changeset_compare_view_content">
381 <div id="changeset_compare_view_content">
377 ##CS
382 ##CS
378 % if c.missing_requirements:
383 % if c.missing_requirements:
379 <div class="box">
384 <div class="box">
380 <div class="alert alert-warning">
385 <div class="alert alert-warning">
381 <div>
386 <div>
382 <strong>${_('Missing requirements:')}</strong>
387 <strong>${_('Missing requirements:')}</strong>
383 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
388 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
384 </div>
389 </div>
385 </div>
390 </div>
386 </div>
391 </div>
387 % elif c.missing_commits:
392 % elif c.missing_commits:
388 <div class="box">
393 <div class="box">
389 <div class="alert alert-warning">
394 <div class="alert alert-warning">
390 <div>
395 <div>
391 <strong>${_('Missing commits')}:</strong>
396 <strong>${_('Missing commits')}:</strong>
392 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
397 ${_('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.')}
398 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
394 </div>
399 </div>
395 </div>
400 </div>
396 </div>
401 </div>
397 % endif
402 % endif
398
403
399 <div class="compare_view_commits_title">
404 <div class="compare_view_commits_title">
400 % if not c.compare_mode:
405 % if not c.compare_mode:
401
406
402 % if c.at_version_pos:
407 % if c.at_version_pos:
403 <h4>
408 <h4>
404 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
409 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
405 </h4>
410 </h4>
406 % endif
411 % endif
407
412
408 <div class="pull-left">
413 <div class="pull-left">
409 <div class="btn-group">
414 <div class="btn-group">
410 <a
415 <a
411 class="btn"
416 class="btn"
412 href="#"
417 href="#"
413 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
418 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)}
419 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
415 </a>
420 </a>
416 <a
421 <a
417 class="btn"
422 class="btn"
418 href="#"
423 href="#"
419 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
424 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)}
425 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
421 </a>
426 </a>
422 </div>
427 </div>
423 </div>
428 </div>
424
429
425 <div class="pull-right">
430 <div class="pull-right">
426 % if c.allowed_to_update and not c.pull_request.is_closed():
431 % 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>
432 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
428 % else:
433 % else:
429 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
434 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
430 % endif
435 % endif
431
436
432 </div>
437 </div>
433 % endif
438 % endif
434 </div>
439 </div>
435
440
436 % if not c.missing_commits:
441 % if not c.missing_commits:
437 % if c.compare_mode:
442 % if c.compare_mode:
438 % if c.at_version:
443 % if c.at_version:
439 <h4>
444 <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')}:
445 ${_('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>
446 </h4>
442
447
443 <div class="subtitle-compare">
448 <div class="subtitle-compare">
444 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
449 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
445 </div>
450 </div>
446
451
447 <div class="container">
452 <div class="container">
448 <table class="rctable compare_view_commits">
453 <table class="rctable compare_view_commits">
449 <tr>
454 <tr>
450 <th></th>
455 <th></th>
451 <th>${_('Time')}</th>
456 <th>${_('Time')}</th>
452 <th>${_('Author')}</th>
457 <th>${_('Author')}</th>
453 <th>${_('Commit')}</th>
458 <th>${_('Commit')}</th>
454 <th></th>
459 <th></th>
455 <th>${_('Description')}</th>
460 <th>${_('Description')}</th>
456 </tr>
461 </tr>
457
462
458 % for c_type, commit in c.commit_changes:
463 % for c_type, commit in c.commit_changes:
459 % if c_type in ['a', 'r']:
464 % if c_type in ['a', 'r']:
460 <%
465 <%
461 if c_type == 'a':
466 if c_type == 'a':
462 cc_title = _('Commit added in displayed changes')
467 cc_title = _('Commit added in displayed changes')
463 elif c_type == 'r':
468 elif c_type == 'r':
464 cc_title = _('Commit removed in displayed changes')
469 cc_title = _('Commit removed in displayed changes')
465 else:
470 else:
466 cc_title = ''
471 cc_title = ''
467 %>
472 %>
468 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
473 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
469 <td>
474 <td>
470 <div class="commit-change-indicator color-${c_type}-border">
475 <div class="commit-change-indicator color-${c_type}-border">
471 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
476 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
472 ${c_type.upper()}
477 ${c_type.upper()}
473 </div>
478 </div>
474 </div>
479 </div>
475 </td>
480 </td>
476 <td class="td-time">
481 <td class="td-time">
477 ${h.age_component(commit.date)}
482 ${h.age_component(commit.date)}
478 </td>
483 </td>
479 <td class="td-user">
484 <td class="td-user">
480 ${base.gravatar_with_user(commit.author, 16)}
485 ${base.gravatar_with_user(commit.author, 16)}
481 </td>
486 </td>
482 <td class="td-hash">
487 <td class="td-hash">
483 <code>
488 <code>
484 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
489 <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)}
490 r${commit.revision}:${h.short_id(commit.raw_id)}
486 </a>
491 </a>
487 ${h.hidden('revisions', commit.raw_id)}
492 ${h.hidden('revisions', commit.raw_id)}
488 </code>
493 </code>
489 </td>
494 </td>
490 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
495 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
491 <div class="show_more_col">
496 <div class="show_more_col">
492 <i class="show_more"></i>
497 <i class="show_more"></i>
493 </div>
498 </div>
494 </td>
499 </td>
495 <td class="mid td-description">
500 <td class="mid td-description">
496 <div class="log-container truncate-wrap">
501 <div class="log-container truncate-wrap">
497 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
502 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
498 ${h.urlify_commit_message(commit.message, c.repo_name)}
503 ${h.urlify_commit_message(commit.message, c.repo_name)}
499 </div>
504 </div>
500 </div>
505 </div>
501 </td>
506 </td>
502 </tr>
507 </tr>
503 % endif
508 % endif
504 % endfor
509 % endfor
505 </table>
510 </table>
506 </div>
511 </div>
507
512
508 <script>
513 <script>
509 $('.expand_commit').on('click',function(e){
514 $('.expand_commit').on('click',function(e){
510 var target_expand = $(this);
515 var target_expand = $(this);
511 var cid = target_expand.data('commitId');
516 var cid = target_expand.data('commitId');
512
517
513 if (target_expand.hasClass('open')){
518 if (target_expand.hasClass('open')){
514 $('#c-'+cid).css({
519 $('#c-'+cid).css({
515 'height': '1.5em',
520 'height': '1.5em',
516 'white-space': 'nowrap',
521 'white-space': 'nowrap',
517 'text-overflow': 'ellipsis',
522 'text-overflow': 'ellipsis',
518 'overflow':'hidden'
523 'overflow':'hidden'
519 });
524 });
520 target_expand.removeClass('open');
525 target_expand.removeClass('open');
521 }
526 }
522 else {
527 else {
523 $('#c-'+cid).css({
528 $('#c-'+cid).css({
524 'height': 'auto',
529 'height': 'auto',
525 'white-space': 'pre-line',
530 'white-space': 'pre-line',
526 'text-overflow': 'initial',
531 'text-overflow': 'initial',
527 'overflow':'visible'
532 'overflow':'visible'
528 });
533 });
529 target_expand.addClass('open');
534 target_expand.addClass('open');
530 }
535 }
531 });
536 });
532 </script>
537 </script>
533
538
534 % endif
539 % endif
535
540
536 % else:
541 % else:
537 <%include file="/compare/compare_commits.mako" />
542 <%include file="/compare/compare_commits.mako" />
538 % endif
543 % endif
539
544
540 <div class="cs_files">
545 <div class="cs_files">
541 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
546 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
542 ${cbdiffs.render_diffset_menu()}
547 ${cbdiffs.render_diffset_menu()}
543 ${cbdiffs.render_diffset(
548 ${cbdiffs.render_diffset(
544 c.diffset, use_comments=True,
549 c.diffset, use_comments=True,
545 collapse_when_files_over=30,
550 collapse_when_files_over=30,
546 disable_new_comments=not c.allowed_to_comment,
551 disable_new_comments=not c.allowed_to_comment,
547 deleted_files_comments=c.deleted_files_comments)}
552 deleted_files_comments=c.deleted_files_comments)}
548 </div>
553 </div>
549 % else:
554 % else:
550 ## skipping commits we need to clear the view for missing commits
555 ## skipping commits we need to clear the view for missing commits
551 <div style="clear:both;"></div>
556 <div style="clear:both;"></div>
552 % endif
557 % endif
553
558
554 </div>
559 </div>
555 </div>
560 </div>
556
561
557 ## template for inline comment form
562 ## template for inline comment form
558 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
563 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
559
564
560 ## render general comments
565 ## render general comments
561
566
562 <div id="comment-tr-show">
567 <div id="comment-tr-show">
563 <div class="comment">
568 <div class="comment">
564 % if general_outdated_comm_count_ver:
569 % if general_outdated_comm_count_ver:
565 <div class="meta">
570 <div class="meta">
566 % if general_outdated_comm_count_ver == 1:
571 % if general_outdated_comm_count_ver == 1:
567 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
572 ${_('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>
573 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
569 % else:
574 % else:
570 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
575 ${_('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>
576 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
572 % endif
577 % endif
573 </div>
578 </div>
574 % endif
579 % endif
575 </div>
580 </div>
576 </div>
581 </div>
577
582
578 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
583 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
579
584
580 % if not c.pull_request.is_closed():
585 % if not c.pull_request.is_closed():
581 ## merge status, and merge action
586 ## merge status, and merge action
582 <div class="pull-request-merge">
587 <div class="pull-request-merge">
583 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
588 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
584 </div>
589 </div>
585
590
586 ## main comment form and it status
591 ## main comment form and it status
587 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
592 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
588 pull_request_id=c.pull_request.pull_request_id),
593 pull_request_id=c.pull_request.pull_request_id),
589 c.pull_request_review_status,
594 c.pull_request_review_status,
590 is_pull_request=True, change_status=c.allowed_to_change_status)}
595 is_pull_request=True, change_status=c.allowed_to_change_status)}
591 %endif
596 %endif
592
597
593 <script type="text/javascript">
598 <script type="text/javascript">
594 if (location.hash) {
599 if (location.hash) {
595 var result = splitDelimitedHash(location.hash);
600 var result = splitDelimitedHash(location.hash);
596 var line = $('html').find(result.loc);
601 var line = $('html').find(result.loc);
597 // show hidden comments if we use location.hash
602 // show hidden comments if we use location.hash
598 if (line.hasClass('comment-general')) {
603 if (line.hasClass('comment-general')) {
599 $(line).show();
604 $(line).show();
600 } else if (line.hasClass('comment-inline')) {
605 } else if (line.hasClass('comment-inline')) {
601 $(line).show();
606 $(line).show();
602 var $cb = $(line).closest('.cb');
607 var $cb = $(line).closest('.cb');
603 $cb.removeClass('cb-collapsed')
608 $cb.removeClass('cb-collapsed')
604 }
609 }
605 if (line.length > 0){
610 if (line.length > 0){
606 offsetScroll(line, 70);
611 offsetScroll(line, 70);
607 }
612 }
608 }
613 }
609
614
610 versionController = new VersionController();
615 versionController = new VersionController();
611 versionController.init();
616 versionController.init();
612
617
613
618
614 $(function(){
619 $(function(){
615 ReviewerAutoComplete('user');
620 ReviewerAutoComplete('user');
616 // custom code mirror
621 // custom code mirror
617 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
622 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
618
623
619 var PRDetails = {
624 var PRDetails = {
620 editButton: $('#open_edit_pullrequest'),
625 editButton: $('#open_edit_pullrequest'),
621 closeButton: $('#close_edit_pullrequest'),
626 closeButton: $('#close_edit_pullrequest'),
622 deleteButton: $('#delete_pullrequest'),
627 deleteButton: $('#delete_pullrequest'),
623 viewFields: $('#pr-desc, #pr-title'),
628 viewFields: $('#pr-desc, #pr-title'),
624 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
629 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
625
630
626 init: function() {
631 init: function() {
627 var that = this;
632 var that = this;
628 this.editButton.on('click', function(e) { that.edit(); });
633 this.editButton.on('click', function(e) { that.edit(); });
629 this.closeButton.on('click', function(e) { that.view(); });
634 this.closeButton.on('click', function(e) { that.view(); });
630 },
635 },
631
636
632 edit: function(event) {
637 edit: function(event) {
633 this.viewFields.hide();
638 this.viewFields.hide();
634 this.editButton.hide();
639 this.editButton.hide();
635 this.deleteButton.hide();
640 this.deleteButton.hide();
636 this.closeButton.show();
641 this.closeButton.show();
637 this.editFields.show();
642 this.editFields.show();
638 codeMirrorInstance.refresh();
643 codeMirrorInstance.refresh();
639 },
644 },
640
645
641 view: function(event) {
646 view: function(event) {
642 this.editButton.show();
647 this.editButton.show();
643 this.deleteButton.show();
648 this.deleteButton.show();
644 this.editFields.hide();
649 this.editFields.hide();
645 this.closeButton.hide();
650 this.closeButton.hide();
646 this.viewFields.show();
651 this.viewFields.show();
647 }
652 }
648 };
653 };
649
654
650 var ReviewersPanel = {
655 var ReviewersPanel = {
651 editButton: $('#open_edit_reviewers'),
656 editButton: $('#open_edit_reviewers'),
652 closeButton: $('#close_edit_reviewers'),
657 closeButton: $('#close_edit_reviewers'),
653 addButton: $('#add_reviewer_input'),
658 addButton: $('#add_reviewer_input'),
654 removeButtons: $('.reviewer_member_remove'),
659 removeButtons: $('.reviewer_member_remove'),
655
660
656 init: function() {
661 init: function() {
657 var that = this;
662 var that = this;
658 this.editButton.on('click', function(e) { that.edit(); });
663 this.editButton.on('click', function(e) { that.edit(); });
659 this.closeButton.on('click', function(e) { that.close(); });
664 this.closeButton.on('click', function(e) { that.close(); });
660 },
665 },
661
666
662 edit: function(event) {
667 edit: function(event) {
663 this.editButton.hide();
668 this.editButton.hide();
664 this.closeButton.show();
669 this.closeButton.show();
665 this.addButton.show();
670 this.addButton.show();
666 this.removeButtons.css('visibility', 'visible');
671 this.removeButtons.css('visibility', 'visible');
667 },
672 },
668
673
669 close: function(event) {
674 close: function(event) {
670 this.editButton.show();
675 this.editButton.show();
671 this.closeButton.hide();
676 this.closeButton.hide();
672 this.addButton.hide();
677 this.addButton.hide();
673 this.removeButtons.css('visibility', 'hidden');
678 this.removeButtons.css('visibility', 'hidden');
674 }
679 }
675 };
680 };
676
681
677 PRDetails.init();
682 PRDetails.init();
678 ReviewersPanel.init();
683 ReviewersPanel.init();
679
684
680 showOutdated = function(self){
685 showOutdated = function(self){
681 $('.comment-inline.comment-outdated').show();
686 $('.comment-inline.comment-outdated').show();
682 $('.filediff-outdated').show();
687 $('.filediff-outdated').show();
683 $('.showOutdatedComments').hide();
688 $('.showOutdatedComments').hide();
684 $('.hideOutdatedComments').show();
689 $('.hideOutdatedComments').show();
685 };
690 };
686
691
687 hideOutdated = function(self){
692 hideOutdated = function(self){
688 $('.comment-inline.comment-outdated').hide();
693 $('.comment-inline.comment-outdated').hide();
689 $('.filediff-outdated').hide();
694 $('.filediff-outdated').hide();
690 $('.hideOutdatedComments').hide();
695 $('.hideOutdatedComments').hide();
691 $('.showOutdatedComments').show();
696 $('.showOutdatedComments').show();
692 };
697 };
693
698
694 refreshMergeChecks = function(){
699 refreshMergeChecks = function(){
695 var loadUrl = "${h.url.current(merge_checks=1)}";
700 var loadUrl = "${h.url.current(merge_checks=1)}";
696 $('.pull-request-merge').css('opacity', 0.3);
701 $('.pull-request-merge').css('opacity', 0.3);
697 $('.action-buttons-extra').css('opacity', 0.3);
702 $('.action-buttons-extra').css('opacity', 0.3);
698
703
699 $('.pull-request-merge').load(
704 $('.pull-request-merge').load(
700 loadUrl, function() {
705 loadUrl, function() {
701 $('.pull-request-merge').css('opacity', 1);
706 $('.pull-request-merge').css('opacity', 1);
702
707
703 $('.action-buttons-extra').css('opacity', 1);
708 $('.action-buttons-extra').css('opacity', 1);
704 injectCloseAction();
709 injectCloseAction();
705 }
710 }
706 );
711 );
707 };
712 };
708
713
709 injectCloseAction = function() {
714 injectCloseAction = function() {
710 var closeAction = $('#close-pull-request-action').html();
715 var closeAction = $('#close-pull-request-action').html();
711 var $actionButtons = $('.action-buttons-extra');
716 var $actionButtons = $('.action-buttons-extra');
712 // clear the action before
717 // clear the action before
713 $actionButtons.html("");
718 $actionButtons.html("");
714 $actionButtons.html(closeAction);
719 $actionButtons.html(closeAction);
715 };
720 };
716
721
717 closePullRequest = function (status) {
722 closePullRequest = function (status) {
718 // inject closing flag
723 // inject closing flag
719 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
724 $('.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');
725 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
721 $(generalCommentForm.submitForm).submit();
726 $(generalCommentForm.submitForm).submit();
722 };
727 };
723
728
724 $('#show-outdated-comments').on('click', function(e){
729 $('#show-outdated-comments').on('click', function(e){
725 var button = $(this);
730 var button = $(this);
726 var outdated = $('.comment-outdated');
731 var outdated = $('.comment-outdated');
727
732
728 if (button.html() === "(Show)") {
733 if (button.html() === "(Show)") {
729 button.html("(Hide)");
734 button.html("(Hide)");
730 outdated.show();
735 outdated.show();
731 } else {
736 } else {
732 button.html("(Show)");
737 button.html("(Show)");
733 outdated.hide();
738 outdated.hide();
734 }
739 }
735 });
740 });
736
741
737 $('.show-inline-comments').on('change', function(e){
742 $('.show-inline-comments').on('change', function(e){
738 var show = 'none';
743 var show = 'none';
739 var target = e.currentTarget;
744 var target = e.currentTarget;
740 if(target.checked){
745 if(target.checked){
741 show = ''
746 show = ''
742 }
747 }
743 var boxid = $(target).attr('id_for');
748 var boxid = $(target).attr('id_for');
744 var comments = $('#{0} .inline-comments'.format(boxid));
749 var comments = $('#{0} .inline-comments'.format(boxid));
745 var fn_display = function(idx){
750 var fn_display = function(idx){
746 $(this).css('display', show);
751 $(this).css('display', show);
747 };
752 };
748 $(comments).each(fn_display);
753 $(comments).each(fn_display);
749 var btns = $('#{0} .inline-comments-button'.format(boxid));
754 var btns = $('#{0} .inline-comments-button'.format(boxid));
750 $(btns).each(fn_display);
755 $(btns).each(fn_display);
751 });
756 });
752
757
753 $('#merge_pull_request_form').submit(function() {
758 $('#merge_pull_request_form').submit(function() {
754 if (!$('#merge_pull_request').attr('disabled')) {
759 if (!$('#merge_pull_request').attr('disabled')) {
755 $('#merge_pull_request').attr('disabled', 'disabled');
760 $('#merge_pull_request').attr('disabled', 'disabled');
756 }
761 }
757 return true;
762 return true;
758 });
763 });
759
764
760 $('#edit_pull_request').on('click', function(e){
765 $('#edit_pull_request').on('click', function(e){
761 var title = $('#pr-title-input').val();
766 var title = $('#pr-title-input').val();
762 var description = codeMirrorInstance.getValue();
767 var description = codeMirrorInstance.getValue();
763 editPullRequest(
768 editPullRequest(
764 "${c.repo_name}", "${c.pull_request.pull_request_id}",
769 "${c.repo_name}", "${c.pull_request.pull_request_id}",
765 title, description);
770 title, description);
766 });
771 });
767
772
768 $('#update_pull_request').on('click', function(e){
773 $('#update_pull_request').on('click', function(e){
769 $(this).attr('disabled', 'disabled');
774 $(this).attr('disabled', 'disabled');
770 $(this).addClass('disabled');
775 $(this).addClass('disabled');
771 $(this).html(_gettext('saving...'));
776 $(this).html(_gettext('saving...'));
772 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
777 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
773 });
778 });
774
779
775 $('#update_commits').on('click', function(e){
780 $('#update_commits').on('click', function(e){
776 var isDisabled = !$(e.currentTarget).attr('disabled');
781 var isDisabled = !$(e.currentTarget).attr('disabled');
777 $(e.currentTarget).text(_gettext('Updating...'));
782 $(e.currentTarget).text(_gettext('Updating...'));
778 $(e.currentTarget).attr('disabled', 'disabled');
783 $(e.currentTarget).attr('disabled', 'disabled');
779 if(isDisabled){
784 if(isDisabled){
780 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
785 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
781 }
786 }
782
787
783 });
788 });
784 // fixing issue with caches on firefox
789 // fixing issue with caches on firefox
785 $('#update_commits').removeAttr("disabled");
790 $('#update_commits').removeAttr("disabled");
786
791
787 $('#close_pull_request').on('click', function(e){
792 $('#close_pull_request').on('click', function(e){
788 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
793 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
789 });
794 });
790
795
791 $('.show-inline-comments').on('click', function(e){
796 $('.show-inline-comments').on('click', function(e){
792 var boxid = $(this).attr('data-comment-id');
797 var boxid = $(this).attr('data-comment-id');
793 var button = $(this);
798 var button = $(this);
794
799
795 if(button.hasClass("comments-visible")) {
800 if(button.hasClass("comments-visible")) {
796 $('#{0} .inline-comments'.format(boxid)).each(function(index){
801 $('#{0} .inline-comments'.format(boxid)).each(function(index){
797 $(this).hide();
802 $(this).hide();
798 });
803 });
799 button.removeClass("comments-visible");
804 button.removeClass("comments-visible");
800 } else {
805 } else {
801 $('#{0} .inline-comments'.format(boxid)).each(function(index){
806 $('#{0} .inline-comments'.format(boxid)).each(function(index){
802 $(this).show();
807 $(this).show();
803 });
808 });
804 button.addClass("comments-visible");
809 button.addClass("comments-visible");
805 }
810 }
806 });
811 });
807
812
808 // register submit callback on commentForm form to track TODOs
813 // register submit callback on commentForm form to track TODOs
809 window.commentFormGlobalSubmitSuccessCallback = function(){
814 window.commentFormGlobalSubmitSuccessCallback = function(){
810 refreshMergeChecks();
815 refreshMergeChecks();
811 };
816 };
812 // initial injection
817 // initial injection
813 injectCloseAction();
818 injectCloseAction();
814
819
815 })
820 })
816 </script>
821 </script>
817
822
818 </div>
823 </div>
819 </div>
824 </div>
820
825
821 </%def>
826 </%def>
General Comments 0
You need to be logged in to leave comments. Login now