##// END OF EJS Templates
pull-requests: change the update commits logic to handle target changes better....
marcink -
r1601:a639657c default
parent child Browse files
Show More
@@ -1,1071 +1,1083 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
446 if resp.target_changed and resp.source_changed:
447 changed = 'target and source repositories'
448 elif resp.target_changed and not resp.source_changed:
449 changed = 'target repository'
450 elif not resp.target_changed and resp.source_changed:
451 changed = 'source repository'
452 else:
453 changed = 'nothing'
454
445 msg = _(
455 msg = _(
446 u'Pull request updated to "{source_commit_id}" with '
456 u'Pull request updated to "{source_commit_id}" with '
447 u'{count_added} added, {count_removed} removed commits.')
457 u'{count_added} added, {count_removed} removed commits. '
458 u'Source of changes: {change_source}')
448 msg = msg.format(
459 msg = msg.format(
449 source_commit_id=pull_request.source_ref_parts.commit_id,
460 source_commit_id=pull_request.source_ref_parts.commit_id,
450 count_added=len(resp.changes.added),
461 count_added=len(resp.changes.added),
451 count_removed=len(resp.changes.removed))
462 count_removed=len(resp.changes.removed),
463 change_source=changed)
452 h.flash(msg, category='success')
464 h.flash(msg, category='success')
453
465
454 registry = get_current_registry()
466 registry = get_current_registry()
455 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
467 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
456 channelstream_config = rhodecode_plugins.get('channelstream', {})
468 channelstream_config = rhodecode_plugins.get('channelstream', {})
457 if channelstream_config.get('enabled'):
469 if channelstream_config.get('enabled'):
458 message = msg + (
470 message = msg + (
459 ' - <a onclick="window.location.reload()">'
471 ' - <a onclick="window.location.reload()">'
460 '<strong>{}</strong></a>'.format(_('Reload page')))
472 '<strong>{}</strong></a>'.format(_('Reload page')))
461 channel = '/repo${}$/pr/{}'.format(
473 channel = '/repo${}$/pr/{}'.format(
462 pull_request.target_repo.repo_name,
474 pull_request.target_repo.repo_name,
463 pull_request.pull_request_id
475 pull_request.pull_request_id
464 )
476 )
465 payload = {
477 payload = {
466 'type': 'message',
478 'type': 'message',
467 'user': 'system',
479 'user': 'system',
468 'exclude_users': [request.user.username],
480 'exclude_users': [request.user.username],
469 'channel': channel,
481 'channel': channel,
470 'message': {
482 'message': {
471 'message': message,
483 'message': message,
472 'level': 'success',
484 'level': 'success',
473 'topic': '/notifications'
485 'topic': '/notifications'
474 }
486 }
475 }
487 }
476 channelstream_request(
488 channelstream_request(
477 channelstream_config, [payload], '/message',
489 channelstream_config, [payload], '/message',
478 raise_exc=False)
490 raise_exc=False)
479 else:
491 else:
480 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
492 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
481 warning_reasons = [
493 warning_reasons = [
482 UpdateFailureReason.NO_CHANGE,
494 UpdateFailureReason.NO_CHANGE,
483 UpdateFailureReason.WRONG_REF_TPYE,
495 UpdateFailureReason.WRONG_REF_TPYE,
484 ]
496 ]
485 category = 'warning' if resp.reason in warning_reasons else 'error'
497 category = 'warning' if resp.reason in warning_reasons else 'error'
486 h.flash(msg, category=category)
498 h.flash(msg, category=category)
487
499
488 @auth.CSRFRequired()
500 @auth.CSRFRequired()
489 @LoginRequired()
501 @LoginRequired()
490 @NotAnonymous()
502 @NotAnonymous()
491 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
503 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
492 'repository.admin')
504 'repository.admin')
493 def merge(self, repo_name, pull_request_id):
505 def merge(self, repo_name, pull_request_id):
494 """
506 """
495 POST /{repo_name}/pull-request/{pull_request_id}
507 POST /{repo_name}/pull-request/{pull_request_id}
496
508
497 Merge will perform a server-side merge of the specified
509 Merge will perform a server-side merge of the specified
498 pull request, if the pull request is approved and mergeable.
510 pull request, if the pull request is approved and mergeable.
499 After successful merging, the pull request is automatically
511 After successful merging, the pull request is automatically
500 closed, with a relevant comment.
512 closed, with a relevant comment.
501 """
513 """
502 pull_request_id = safe_int(pull_request_id)
514 pull_request_id = safe_int(pull_request_id)
503 pull_request = PullRequest.get_or_404(pull_request_id)
515 pull_request = PullRequest.get_or_404(pull_request_id)
504 user = c.rhodecode_user
516 user = c.rhodecode_user
505
517
506 check = MergeCheck.validate(pull_request, user)
518 check = MergeCheck.validate(pull_request, user)
507 merge_possible = not check.failed
519 merge_possible = not check.failed
508
520
509 for err_type, error_msg in check.errors:
521 for err_type, error_msg in check.errors:
510 h.flash(error_msg, category=err_type)
522 h.flash(error_msg, category=err_type)
511
523
512 if merge_possible:
524 if merge_possible:
513 log.debug("Pre-conditions checked, trying to merge.")
525 log.debug("Pre-conditions checked, trying to merge.")
514 extras = vcs_operation_context(
526 extras = vcs_operation_context(
515 request.environ, repo_name=pull_request.target_repo.repo_name,
527 request.environ, repo_name=pull_request.target_repo.repo_name,
516 username=user.username, action='push',
528 username=user.username, action='push',
517 scm=pull_request.target_repo.repo_type)
529 scm=pull_request.target_repo.repo_type)
518 self._merge_pull_request(pull_request, user, extras)
530 self._merge_pull_request(pull_request, user, extras)
519
531
520 return redirect(url(
532 return redirect(url(
521 'pullrequest_show',
533 'pullrequest_show',
522 repo_name=pull_request.target_repo.repo_name,
534 repo_name=pull_request.target_repo.repo_name,
523 pull_request_id=pull_request.pull_request_id))
535 pull_request_id=pull_request.pull_request_id))
524
536
525 def _merge_pull_request(self, pull_request, user, extras):
537 def _merge_pull_request(self, pull_request, user, extras):
526 merge_resp = PullRequestModel().merge(
538 merge_resp = PullRequestModel().merge(
527 pull_request, user, extras=extras)
539 pull_request, user, extras=extras)
528
540
529 if merge_resp.executed:
541 if merge_resp.executed:
530 log.debug("The merge was successful, closing the pull request.")
542 log.debug("The merge was successful, closing the pull request.")
531 PullRequestModel().close_pull_request(
543 PullRequestModel().close_pull_request(
532 pull_request.pull_request_id, user)
544 pull_request.pull_request_id, user)
533 Session().commit()
545 Session().commit()
534 msg = _('Pull request was successfully merged and closed.')
546 msg = _('Pull request was successfully merged and closed.')
535 h.flash(msg, category='success')
547 h.flash(msg, category='success')
536 else:
548 else:
537 log.debug(
549 log.debug(
538 "The merge was not successful. Merge response: %s",
550 "The merge was not successful. Merge response: %s",
539 merge_resp)
551 merge_resp)
540 msg = PullRequestModel().merge_status_message(
552 msg = PullRequestModel().merge_status_message(
541 merge_resp.failure_reason)
553 merge_resp.failure_reason)
542 h.flash(msg, category='error')
554 h.flash(msg, category='error')
543
555
544 def _update_reviewers(self, pull_request_id, review_members):
556 def _update_reviewers(self, pull_request_id, review_members):
545 reviewers = [
557 reviewers = [
546 (int(r['user_id']), r['reasons']) for r in review_members]
558 (int(r['user_id']), r['reasons']) for r in review_members]
547 PullRequestModel().update_reviewers(pull_request_id, reviewers)
559 PullRequestModel().update_reviewers(pull_request_id, reviewers)
548 Session().commit()
560 Session().commit()
549
561
550 def _reject_close(self, pull_request):
562 def _reject_close(self, pull_request):
551 if pull_request.is_closed():
563 if pull_request.is_closed():
552 raise HTTPForbidden()
564 raise HTTPForbidden()
553
565
554 PullRequestModel().close_pull_request_with_comment(
566 PullRequestModel().close_pull_request_with_comment(
555 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
567 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
556 Session().commit()
568 Session().commit()
557
569
558 @LoginRequired()
570 @LoginRequired()
559 @NotAnonymous()
571 @NotAnonymous()
560 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
572 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
561 'repository.admin')
573 'repository.admin')
562 @auth.CSRFRequired()
574 @auth.CSRFRequired()
563 @jsonify
575 @jsonify
564 def delete(self, repo_name, pull_request_id):
576 def delete(self, repo_name, pull_request_id):
565 pull_request_id = safe_int(pull_request_id)
577 pull_request_id = safe_int(pull_request_id)
566 pull_request = PullRequest.get_or_404(pull_request_id)
578 pull_request = PullRequest.get_or_404(pull_request_id)
567 # only owner can delete it !
579 # only owner can delete it !
568 if pull_request.author.user_id == c.rhodecode_user.user_id:
580 if pull_request.author.user_id == c.rhodecode_user.user_id:
569 PullRequestModel().delete(pull_request)
581 PullRequestModel().delete(pull_request)
570 Session().commit()
582 Session().commit()
571 h.flash(_('Successfully deleted pull request'),
583 h.flash(_('Successfully deleted pull request'),
572 category='success')
584 category='success')
573 return redirect(url('my_account_pullrequests'))
585 return redirect(url('my_account_pullrequests'))
574 raise HTTPForbidden()
586 raise HTTPForbidden()
575
587
576 def _get_pr_version(self, pull_request_id, version=None):
588 def _get_pr_version(self, pull_request_id, version=None):
577 pull_request_id = safe_int(pull_request_id)
589 pull_request_id = safe_int(pull_request_id)
578 at_version = None
590 at_version = None
579
591
580 if version and version == 'latest':
592 if version and version == 'latest':
581 pull_request_ver = PullRequest.get(pull_request_id)
593 pull_request_ver = PullRequest.get(pull_request_id)
582 pull_request_obj = pull_request_ver
594 pull_request_obj = pull_request_ver
583 _org_pull_request_obj = pull_request_obj
595 _org_pull_request_obj = pull_request_obj
584 at_version = 'latest'
596 at_version = 'latest'
585 elif version:
597 elif version:
586 pull_request_ver = PullRequestVersion.get_or_404(version)
598 pull_request_ver = PullRequestVersion.get_or_404(version)
587 pull_request_obj = pull_request_ver
599 pull_request_obj = pull_request_ver
588 _org_pull_request_obj = pull_request_ver.pull_request
600 _org_pull_request_obj = pull_request_ver.pull_request
589 at_version = pull_request_ver.pull_request_version_id
601 at_version = pull_request_ver.pull_request_version_id
590 else:
602 else:
591 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
603 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
592
604
593 pull_request_display_obj = PullRequest.get_pr_display_object(
605 pull_request_display_obj = PullRequest.get_pr_display_object(
594 pull_request_obj, _org_pull_request_obj)
606 pull_request_obj, _org_pull_request_obj)
595
607
596 return _org_pull_request_obj, pull_request_obj, \
608 return _org_pull_request_obj, pull_request_obj, \
597 pull_request_display_obj, at_version
609 pull_request_display_obj, at_version
598
610
599 def _get_diffset(
611 def _get_diffset(
600 self, source_repo, source_ref_id, target_ref_id, target_commit,
612 self, source_repo, source_ref_id, target_ref_id, target_commit,
601 source_commit, diff_limit, file_limit, display_inline_comments):
613 source_commit, diff_limit, file_limit, display_inline_comments):
602 vcs_diff = PullRequestModel().get_diff(
614 vcs_diff = PullRequestModel().get_diff(
603 source_repo, source_ref_id, target_ref_id)
615 source_repo, source_ref_id, target_ref_id)
604
616
605 diff_processor = diffs.DiffProcessor(
617 diff_processor = diffs.DiffProcessor(
606 vcs_diff, format='newdiff', diff_limit=diff_limit,
618 vcs_diff, format='newdiff', diff_limit=diff_limit,
607 file_limit=file_limit, show_full_diff=c.fulldiff)
619 file_limit=file_limit, show_full_diff=c.fulldiff)
608
620
609 _parsed = diff_processor.prepare()
621 _parsed = diff_processor.prepare()
610
622
611 def _node_getter(commit):
623 def _node_getter(commit):
612 def get_node(fname):
624 def get_node(fname):
613 try:
625 try:
614 return commit.get_node(fname)
626 return commit.get_node(fname)
615 except NodeDoesNotExistError:
627 except NodeDoesNotExistError:
616 return None
628 return None
617
629
618 return get_node
630 return get_node
619
631
620 diffset = codeblocks.DiffSet(
632 diffset = codeblocks.DiffSet(
621 repo_name=c.repo_name,
633 repo_name=c.repo_name,
622 source_repo_name=c.source_repo.repo_name,
634 source_repo_name=c.source_repo.repo_name,
623 source_node_getter=_node_getter(target_commit),
635 source_node_getter=_node_getter(target_commit),
624 target_node_getter=_node_getter(source_commit),
636 target_node_getter=_node_getter(source_commit),
625 comments=display_inline_comments
637 comments=display_inline_comments
626 )
638 )
627 diffset = diffset.render_patchset(
639 diffset = diffset.render_patchset(
628 _parsed, target_commit.raw_id, source_commit.raw_id)
640 _parsed, target_commit.raw_id, source_commit.raw_id)
629
641
630 return diffset
642 return diffset
631
643
632 @LoginRequired()
644 @LoginRequired()
633 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
645 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
634 'repository.admin')
646 'repository.admin')
635 def show(self, repo_name, pull_request_id):
647 def show(self, repo_name, pull_request_id):
636 pull_request_id = safe_int(pull_request_id)
648 pull_request_id = safe_int(pull_request_id)
637 version = request.GET.get('version')
649 version = request.GET.get('version')
638 from_version = request.GET.get('from_version') or version
650 from_version = request.GET.get('from_version') or version
639 merge_checks = request.GET.get('merge_checks')
651 merge_checks = request.GET.get('merge_checks')
640 c.fulldiff = str2bool(request.GET.get('fulldiff'))
652 c.fulldiff = str2bool(request.GET.get('fulldiff'))
641
653
642 (pull_request_latest,
654 (pull_request_latest,
643 pull_request_at_ver,
655 pull_request_at_ver,
644 pull_request_display_obj,
656 pull_request_display_obj,
645 at_version) = self._get_pr_version(
657 at_version) = self._get_pr_version(
646 pull_request_id, version=version)
658 pull_request_id, version=version)
647 pr_closed = pull_request_latest.is_closed()
659 pr_closed = pull_request_latest.is_closed()
648
660
649 if pr_closed and (version or from_version):
661 if pr_closed and (version or from_version):
650 # not allow to browse versions
662 # not allow to browse versions
651 return redirect(h.url('pullrequest_show', repo_name=repo_name,
663 return redirect(h.url('pullrequest_show', repo_name=repo_name,
652 pull_request_id=pull_request_id))
664 pull_request_id=pull_request_id))
653
665
654 versions = pull_request_display_obj.versions()
666 versions = pull_request_display_obj.versions()
655
667
656 c.at_version = at_version
668 c.at_version = at_version
657 c.at_version_num = (at_version
669 c.at_version_num = (at_version
658 if at_version and at_version != 'latest'
670 if at_version and at_version != 'latest'
659 else None)
671 else None)
660 c.at_version_pos = ChangesetComment.get_index_from_version(
672 c.at_version_pos = ChangesetComment.get_index_from_version(
661 c.at_version_num, versions)
673 c.at_version_num, versions)
662
674
663 (prev_pull_request_latest,
675 (prev_pull_request_latest,
664 prev_pull_request_at_ver,
676 prev_pull_request_at_ver,
665 prev_pull_request_display_obj,
677 prev_pull_request_display_obj,
666 prev_at_version) = self._get_pr_version(
678 prev_at_version) = self._get_pr_version(
667 pull_request_id, version=from_version)
679 pull_request_id, version=from_version)
668
680
669 c.from_version = prev_at_version
681 c.from_version = prev_at_version
670 c.from_version_num = (prev_at_version
682 c.from_version_num = (prev_at_version
671 if prev_at_version and prev_at_version != 'latest'
683 if prev_at_version and prev_at_version != 'latest'
672 else None)
684 else None)
673 c.from_version_pos = ChangesetComment.get_index_from_version(
685 c.from_version_pos = ChangesetComment.get_index_from_version(
674 c.from_version_num, versions)
686 c.from_version_num, versions)
675
687
676 # define if we're in COMPARE mode or VIEW at version mode
688 # define if we're in COMPARE mode or VIEW at version mode
677 compare = at_version != prev_at_version
689 compare = at_version != prev_at_version
678
690
679 # pull_requests repo_name we opened it against
691 # pull_requests repo_name we opened it against
680 # ie. target_repo must match
692 # ie. target_repo must match
681 if repo_name != pull_request_at_ver.target_repo.repo_name:
693 if repo_name != pull_request_at_ver.target_repo.repo_name:
682 raise HTTPNotFound
694 raise HTTPNotFound
683
695
684 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
696 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
685 pull_request_at_ver)
697 pull_request_at_ver)
686
698
687 c.pull_request = pull_request_display_obj
699 c.pull_request = pull_request_display_obj
688 c.pull_request_latest = pull_request_latest
700 c.pull_request_latest = pull_request_latest
689
701
690 if compare or (at_version and not at_version == 'latest'):
702 if compare or (at_version and not at_version == 'latest'):
691 c.allowed_to_change_status = False
703 c.allowed_to_change_status = False
692 c.allowed_to_update = False
704 c.allowed_to_update = False
693 c.allowed_to_merge = False
705 c.allowed_to_merge = False
694 c.allowed_to_delete = False
706 c.allowed_to_delete = False
695 c.allowed_to_comment = False
707 c.allowed_to_comment = False
696 c.allowed_to_close = False
708 c.allowed_to_close = False
697 else:
709 else:
698 c.allowed_to_change_status = PullRequestModel(). \
710 c.allowed_to_change_status = PullRequestModel(). \
699 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
711 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
700 and not pr_closed
712 and not pr_closed
701
713
702 c.allowed_to_update = PullRequestModel().check_user_update(
714 c.allowed_to_update = PullRequestModel().check_user_update(
703 pull_request_latest, c.rhodecode_user) and not pr_closed
715 pull_request_latest, c.rhodecode_user) and not pr_closed
704 c.allowed_to_merge = PullRequestModel().check_user_merge(
716 c.allowed_to_merge = PullRequestModel().check_user_merge(
705 pull_request_latest, c.rhodecode_user) and not pr_closed
717 pull_request_latest, c.rhodecode_user) and not pr_closed
706 c.allowed_to_delete = PullRequestModel().check_user_delete(
718 c.allowed_to_delete = PullRequestModel().check_user_delete(
707 pull_request_latest, c.rhodecode_user) and not pr_closed
719 pull_request_latest, c.rhodecode_user) and not pr_closed
708 c.allowed_to_comment = not pr_closed
720 c.allowed_to_comment = not pr_closed
709 c.allowed_to_close = c.allowed_to_change_status and not pr_closed
721 c.allowed_to_close = c.allowed_to_change_status and not pr_closed
710
722
711 # check merge capabilities
723 # check merge capabilities
712 _merge_check = MergeCheck.validate(
724 _merge_check = MergeCheck.validate(
713 pull_request_latest, user=c.rhodecode_user)
725 pull_request_latest, user=c.rhodecode_user)
714 c.pr_merge_errors = _merge_check.error_details
726 c.pr_merge_errors = _merge_check.error_details
715 c.pr_merge_possible = not _merge_check.failed
727 c.pr_merge_possible = not _merge_check.failed
716 c.pr_merge_message = _merge_check.merge_msg
728 c.pr_merge_message = _merge_check.merge_msg
717
729
718 c.pull_request_review_status = _merge_check.review_status
730 c.pull_request_review_status = _merge_check.review_status
719 if merge_checks:
731 if merge_checks:
720 return render('/pullrequests/pullrequest_merge_checks.mako')
732 return render('/pullrequests/pullrequest_merge_checks.mako')
721
733
722 comments_model = CommentsModel()
734 comments_model = CommentsModel()
723
735
724 # reviewers and statuses
736 # reviewers and statuses
725 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
737 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
726 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
738 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
727
739
728 # GENERAL COMMENTS with versions #
740 # GENERAL COMMENTS with versions #
729 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
741 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
730 q = q.order_by(ChangesetComment.comment_id.asc())
742 q = q.order_by(ChangesetComment.comment_id.asc())
731 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
743 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
732
744
733 # pick comments we want to render at current version
745 # pick comments we want to render at current version
734 c.comment_versions = comments_model.aggregate_comments(
746 c.comment_versions = comments_model.aggregate_comments(
735 general_comments, versions, c.at_version_num)
747 general_comments, versions, c.at_version_num)
736 c.comments = c.comment_versions[c.at_version_num]['until']
748 c.comments = c.comment_versions[c.at_version_num]['until']
737
749
738 # INLINE COMMENTS with versions #
750 # INLINE COMMENTS with versions #
739 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
751 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
740 q = q.order_by(ChangesetComment.comment_id.asc())
752 q = q.order_by(ChangesetComment.comment_id.asc())
741 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
753 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
742 c.inline_versions = comments_model.aggregate_comments(
754 c.inline_versions = comments_model.aggregate_comments(
743 inline_comments, versions, c.at_version_num, inline=True)
755 inline_comments, versions, c.at_version_num, inline=True)
744
756
745 # inject latest version
757 # inject latest version
746 latest_ver = PullRequest.get_pr_display_object(
758 latest_ver = PullRequest.get_pr_display_object(
747 pull_request_latest, pull_request_latest)
759 pull_request_latest, pull_request_latest)
748
760
749 c.versions = versions + [latest_ver]
761 c.versions = versions + [latest_ver]
750
762
751 # if we use version, then do not show later comments
763 # if we use version, then do not show later comments
752 # than current version
764 # than current version
753 display_inline_comments = collections.defaultdict(
765 display_inline_comments = collections.defaultdict(
754 lambda: collections.defaultdict(list))
766 lambda: collections.defaultdict(list))
755 for co in inline_comments:
767 for co in inline_comments:
756 if c.at_version_num:
768 if c.at_version_num:
757 # pick comments that are at least UPTO given version, so we
769 # pick comments that are at least UPTO given version, so we
758 # don't render comments for higher version
770 # don't render comments for higher version
759 should_render = co.pull_request_version_id and \
771 should_render = co.pull_request_version_id and \
760 co.pull_request_version_id <= c.at_version_num
772 co.pull_request_version_id <= c.at_version_num
761 else:
773 else:
762 # showing all, for 'latest'
774 # showing all, for 'latest'
763 should_render = True
775 should_render = True
764
776
765 if should_render:
777 if should_render:
766 display_inline_comments[co.f_path][co.line_no].append(co)
778 display_inline_comments[co.f_path][co.line_no].append(co)
767
779
768 # load diff data into template context, if we use compare mode then
780 # load diff data into template context, if we use compare mode then
769 # diff is calculated based on changes between versions of PR
781 # diff is calculated based on changes between versions of PR
770
782
771 source_repo = pull_request_at_ver.source_repo
783 source_repo = pull_request_at_ver.source_repo
772 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
784 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
773
785
774 target_repo = pull_request_at_ver.target_repo
786 target_repo = pull_request_at_ver.target_repo
775 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
787 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
776
788
777 if compare:
789 if compare:
778 # in compare switch the diff base to latest commit from prev version
790 # in compare switch the diff base to latest commit from prev version
779 target_ref_id = prev_pull_request_display_obj.revisions[0]
791 target_ref_id = prev_pull_request_display_obj.revisions[0]
780
792
781 # despite opening commits for bookmarks/branches/tags, we always
793 # despite opening commits for bookmarks/branches/tags, we always
782 # convert this to rev to prevent changes after bookmark or branch change
794 # convert this to rev to prevent changes after bookmark or branch change
783 c.source_ref_type = 'rev'
795 c.source_ref_type = 'rev'
784 c.source_ref = source_ref_id
796 c.source_ref = source_ref_id
785
797
786 c.target_ref_type = 'rev'
798 c.target_ref_type = 'rev'
787 c.target_ref = target_ref_id
799 c.target_ref = target_ref_id
788
800
789 c.source_repo = source_repo
801 c.source_repo = source_repo
790 c.target_repo = target_repo
802 c.target_repo = target_repo
791
803
792 # diff_limit is the old behavior, will cut off the whole diff
804 # diff_limit is the old behavior, will cut off the whole diff
793 # if the limit is applied otherwise will just hide the
805 # if the limit is applied otherwise will just hide the
794 # big files from the front-end
806 # big files from the front-end
795 diff_limit = self.cut_off_limit_diff
807 diff_limit = self.cut_off_limit_diff
796 file_limit = self.cut_off_limit_file
808 file_limit = self.cut_off_limit_file
797
809
798 c.commit_ranges = []
810 c.commit_ranges = []
799 source_commit = EmptyCommit()
811 source_commit = EmptyCommit()
800 target_commit = EmptyCommit()
812 target_commit = EmptyCommit()
801 c.missing_requirements = False
813 c.missing_requirements = False
802
814
803 source_scm = source_repo.scm_instance()
815 source_scm = source_repo.scm_instance()
804 target_scm = target_repo.scm_instance()
816 target_scm = target_repo.scm_instance()
805
817
806 # try first shadow repo, fallback to regular repo
818 # try first shadow repo, fallback to regular repo
807 try:
819 try:
808 commits_source_repo = pull_request_latest.get_shadow_repo()
820 commits_source_repo = pull_request_latest.get_shadow_repo()
809 except Exception:
821 except Exception:
810 log.debug('Failed to get shadow repo', exc_info=True)
822 log.debug('Failed to get shadow repo', exc_info=True)
811 commits_source_repo = source_scm
823 commits_source_repo = source_scm
812
824
813 c.commits_source_repo = commits_source_repo
825 c.commits_source_repo = commits_source_repo
814 commit_cache = {}
826 commit_cache = {}
815 try:
827 try:
816 pre_load = ["author", "branch", "date", "message"]
828 pre_load = ["author", "branch", "date", "message"]
817 show_revs = pull_request_at_ver.revisions
829 show_revs = pull_request_at_ver.revisions
818 for rev in show_revs:
830 for rev in show_revs:
819 comm = commits_source_repo.get_commit(
831 comm = commits_source_repo.get_commit(
820 commit_id=rev, pre_load=pre_load)
832 commit_id=rev, pre_load=pre_load)
821 c.commit_ranges.append(comm)
833 c.commit_ranges.append(comm)
822 commit_cache[comm.raw_id] = comm
834 commit_cache[comm.raw_id] = comm
823
835
824 target_commit = commits_source_repo.get_commit(
836 target_commit = commits_source_repo.get_commit(
825 commit_id=safe_str(target_ref_id))
837 commit_id=safe_str(target_ref_id))
826 source_commit = commits_source_repo.get_commit(
838 source_commit = commits_source_repo.get_commit(
827 commit_id=safe_str(source_ref_id))
839 commit_id=safe_str(source_ref_id))
828 except CommitDoesNotExistError:
840 except CommitDoesNotExistError:
829 pass
841 pass
830 except RepositoryRequirementError:
842 except RepositoryRequirementError:
831 log.warning(
843 log.warning(
832 'Failed to get all required data from repo', exc_info=True)
844 'Failed to get all required data from repo', exc_info=True)
833 c.missing_requirements = True
845 c.missing_requirements = True
834
846
835 c.ancestor = None # set it to None, to hide it from PR view
847 c.ancestor = None # set it to None, to hide it from PR view
836
848
837 try:
849 try:
838 ancestor_id = source_scm.get_common_ancestor(
850 ancestor_id = source_scm.get_common_ancestor(
839 source_commit.raw_id, target_commit.raw_id, target_scm)
851 source_commit.raw_id, target_commit.raw_id, target_scm)
840 c.ancestor_commit = source_scm.get_commit(ancestor_id)
852 c.ancestor_commit = source_scm.get_commit(ancestor_id)
841 except Exception:
853 except Exception:
842 c.ancestor_commit = None
854 c.ancestor_commit = None
843
855
844 c.statuses = source_repo.statuses(
856 c.statuses = source_repo.statuses(
845 [x.raw_id for x in c.commit_ranges])
857 [x.raw_id for x in c.commit_ranges])
846
858
847 # auto collapse if we have more than limit
859 # auto collapse if we have more than limit
848 collapse_limit = diffs.DiffProcessor._collapse_commits_over
860 collapse_limit = diffs.DiffProcessor._collapse_commits_over
849 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
861 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
850 c.compare_mode = compare
862 c.compare_mode = compare
851
863
852 c.missing_commits = False
864 c.missing_commits = False
853 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
865 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
854 or source_commit == target_commit):
866 or source_commit == target_commit):
855
867
856 c.missing_commits = True
868 c.missing_commits = True
857 else:
869 else:
858
870
859 c.diffset = self._get_diffset(
871 c.diffset = self._get_diffset(
860 commits_source_repo, source_ref_id, target_ref_id,
872 commits_source_repo, source_ref_id, target_ref_id,
861 target_commit, source_commit,
873 target_commit, source_commit,
862 diff_limit, file_limit, display_inline_comments)
874 diff_limit, file_limit, display_inline_comments)
863
875
864 c.limited_diff = c.diffset.limited_diff
876 c.limited_diff = c.diffset.limited_diff
865
877
866 # calculate removed files that are bound to comments
878 # calculate removed files that are bound to comments
867 comment_deleted_files = [
879 comment_deleted_files = [
868 fname for fname in display_inline_comments
880 fname for fname in display_inline_comments
869 if fname not in c.diffset.file_stats]
881 if fname not in c.diffset.file_stats]
870
882
871 c.deleted_files_comments = collections.defaultdict(dict)
883 c.deleted_files_comments = collections.defaultdict(dict)
872 for fname, per_line_comments in display_inline_comments.items():
884 for fname, per_line_comments in display_inline_comments.items():
873 if fname in comment_deleted_files:
885 if fname in comment_deleted_files:
874 c.deleted_files_comments[fname]['stats'] = 0
886 c.deleted_files_comments[fname]['stats'] = 0
875 c.deleted_files_comments[fname]['comments'] = list()
887 c.deleted_files_comments[fname]['comments'] = list()
876 for lno, comments in per_line_comments.items():
888 for lno, comments in per_line_comments.items():
877 c.deleted_files_comments[fname]['comments'].extend(
889 c.deleted_files_comments[fname]['comments'].extend(
878 comments)
890 comments)
879
891
880 # this is a hack to properly display links, when creating PR, the
892 # this is a hack to properly display links, when creating PR, the
881 # compare view and others uses different notation, and
893 # compare view and others uses different notation, and
882 # compare_commits.mako renders links based on the target_repo.
894 # compare_commits.mako renders links based on the target_repo.
883 # We need to swap that here to generate it properly on the html side
895 # We need to swap that here to generate it properly on the html side
884 c.target_repo = c.source_repo
896 c.target_repo = c.source_repo
885
897
886 c.commit_statuses = ChangesetStatus.STATUSES
898 c.commit_statuses = ChangesetStatus.STATUSES
887
899
888 c.show_version_changes = not pr_closed
900 c.show_version_changes = not pr_closed
889 if c.show_version_changes:
901 if c.show_version_changes:
890 cur_obj = pull_request_at_ver
902 cur_obj = pull_request_at_ver
891 prev_obj = prev_pull_request_at_ver
903 prev_obj = prev_pull_request_at_ver
892
904
893 old_commit_ids = prev_obj.revisions
905 old_commit_ids = prev_obj.revisions
894 new_commit_ids = cur_obj.revisions
906 new_commit_ids = cur_obj.revisions
895 commit_changes = PullRequestModel()._calculate_commit_id_changes(
907 commit_changes = PullRequestModel()._calculate_commit_id_changes(
896 old_commit_ids, new_commit_ids)
908 old_commit_ids, new_commit_ids)
897 c.commit_changes_summary = commit_changes
909 c.commit_changes_summary = commit_changes
898
910
899 # calculate the diff for commits between versions
911 # calculate the diff for commits between versions
900 c.commit_changes = []
912 c.commit_changes = []
901 mark = lambda cs, fw: list(
913 mark = lambda cs, fw: list(
902 h.itertools.izip_longest([], cs, fillvalue=fw))
914 h.itertools.izip_longest([], cs, fillvalue=fw))
903 for c_type, raw_id in mark(commit_changes.added, 'a') \
915 for c_type, raw_id in mark(commit_changes.added, 'a') \
904 + mark(commit_changes.removed, 'r') \
916 + mark(commit_changes.removed, 'r') \
905 + mark(commit_changes.common, 'c'):
917 + mark(commit_changes.common, 'c'):
906
918
907 if raw_id in commit_cache:
919 if raw_id in commit_cache:
908 commit = commit_cache[raw_id]
920 commit = commit_cache[raw_id]
909 else:
921 else:
910 try:
922 try:
911 commit = commits_source_repo.get_commit(raw_id)
923 commit = commits_source_repo.get_commit(raw_id)
912 except CommitDoesNotExistError:
924 except CommitDoesNotExistError:
913 # in case we fail extracting still use "dummy" commit
925 # in case we fail extracting still use "dummy" commit
914 # for display in commit diff
926 # for display in commit diff
915 commit = h.AttributeDict(
927 commit = h.AttributeDict(
916 {'raw_id': raw_id,
928 {'raw_id': raw_id,
917 'message': 'EMPTY or MISSING COMMIT'})
929 'message': 'EMPTY or MISSING COMMIT'})
918 c.commit_changes.append([c_type, commit])
930 c.commit_changes.append([c_type, commit])
919
931
920 # current user review statuses for each version
932 # current user review statuses for each version
921 c.review_versions = {}
933 c.review_versions = {}
922 if c.rhodecode_user.user_id in allowed_reviewers:
934 if c.rhodecode_user.user_id in allowed_reviewers:
923 for co in general_comments:
935 for co in general_comments:
924 if co.author.user_id == c.rhodecode_user.user_id:
936 if co.author.user_id == c.rhodecode_user.user_id:
925 # each comment has a status change
937 # each comment has a status change
926 status = co.status_change
938 status = co.status_change
927 if status:
939 if status:
928 _ver_pr = status[0].comment.pull_request_version_id
940 _ver_pr = status[0].comment.pull_request_version_id
929 c.review_versions[_ver_pr] = status[0]
941 c.review_versions[_ver_pr] = status[0]
930
942
931 return render('/pullrequests/pullrequest_show.mako')
943 return render('/pullrequests/pullrequest_show.mako')
932
944
933 @LoginRequired()
945 @LoginRequired()
934 @NotAnonymous()
946 @NotAnonymous()
935 @HasRepoPermissionAnyDecorator(
947 @HasRepoPermissionAnyDecorator(
936 'repository.read', 'repository.write', 'repository.admin')
948 'repository.read', 'repository.write', 'repository.admin')
937 @auth.CSRFRequired()
949 @auth.CSRFRequired()
938 @jsonify
950 @jsonify
939 def comment(self, repo_name, pull_request_id):
951 def comment(self, repo_name, pull_request_id):
940 pull_request_id = safe_int(pull_request_id)
952 pull_request_id = safe_int(pull_request_id)
941 pull_request = PullRequest.get_or_404(pull_request_id)
953 pull_request = PullRequest.get_or_404(pull_request_id)
942 if pull_request.is_closed():
954 if pull_request.is_closed():
943 raise HTTPForbidden()
955 raise HTTPForbidden()
944
956
945 status = request.POST.get('changeset_status', None)
957 status = request.POST.get('changeset_status', None)
946 text = request.POST.get('text')
958 text = request.POST.get('text')
947 comment_type = request.POST.get('comment_type')
959 comment_type = request.POST.get('comment_type')
948 resolves_comment_id = request.POST.get('resolves_comment_id', None)
960 resolves_comment_id = request.POST.get('resolves_comment_id', None)
949 close_pull_request = request.POST.get('close_pull_request')
961 close_pull_request = request.POST.get('close_pull_request')
950
962
951 close_pr = False
963 close_pr = False
952 if close_pull_request:
964 if close_pull_request:
953 close_pr = True
965 close_pr = True
954 pull_request_review_status = pull_request.calculated_review_status()
966 pull_request_review_status = pull_request.calculated_review_status()
955 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
967 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
956 # approved only if we have voting consent
968 # approved only if we have voting consent
957 status = ChangesetStatus.STATUS_APPROVED
969 status = ChangesetStatus.STATUS_APPROVED
958 else:
970 else:
959 status = ChangesetStatus.STATUS_REJECTED
971 status = ChangesetStatus.STATUS_REJECTED
960
972
961 allowed_to_change_status = PullRequestModel().check_user_change_status(
973 allowed_to_change_status = PullRequestModel().check_user_change_status(
962 pull_request, c.rhodecode_user)
974 pull_request, c.rhodecode_user)
963
975
964 if status and allowed_to_change_status:
976 if status and allowed_to_change_status:
965 message = (_('Status change %(transition_icon)s %(status)s')
977 message = (_('Status change %(transition_icon)s %(status)s')
966 % {'transition_icon': '>',
978 % {'transition_icon': '>',
967 'status': ChangesetStatus.get_status_lbl(status)})
979 'status': ChangesetStatus.get_status_lbl(status)})
968 if close_pr:
980 if close_pr:
969 message = _('Closing with') + ' ' + message
981 message = _('Closing with') + ' ' + message
970 text = text or message
982 text = text or message
971 comm = CommentsModel().create(
983 comm = CommentsModel().create(
972 text=text,
984 text=text,
973 repo=c.rhodecode_db_repo.repo_id,
985 repo=c.rhodecode_db_repo.repo_id,
974 user=c.rhodecode_user.user_id,
986 user=c.rhodecode_user.user_id,
975 pull_request=pull_request_id,
987 pull_request=pull_request_id,
976 f_path=request.POST.get('f_path'),
988 f_path=request.POST.get('f_path'),
977 line_no=request.POST.get('line'),
989 line_no=request.POST.get('line'),
978 status_change=(ChangesetStatus.get_status_lbl(status)
990 status_change=(ChangesetStatus.get_status_lbl(status)
979 if status and allowed_to_change_status else None),
991 if status and allowed_to_change_status else None),
980 status_change_type=(status
992 status_change_type=(status
981 if status and allowed_to_change_status else None),
993 if status and allowed_to_change_status else None),
982 closing_pr=close_pr,
994 closing_pr=close_pr,
983 comment_type=comment_type,
995 comment_type=comment_type,
984 resolves_comment_id=resolves_comment_id
996 resolves_comment_id=resolves_comment_id
985 )
997 )
986
998
987 if allowed_to_change_status:
999 if allowed_to_change_status:
988 old_calculated_status = pull_request.calculated_review_status()
1000 old_calculated_status = pull_request.calculated_review_status()
989 # get status if set !
1001 # get status if set !
990 if status:
1002 if status:
991 ChangesetStatusModel().set_status(
1003 ChangesetStatusModel().set_status(
992 c.rhodecode_db_repo.repo_id,
1004 c.rhodecode_db_repo.repo_id,
993 status,
1005 status,
994 c.rhodecode_user.user_id,
1006 c.rhodecode_user.user_id,
995 comm,
1007 comm,
996 pull_request=pull_request_id
1008 pull_request=pull_request_id
997 )
1009 )
998
1010
999 Session().flush()
1011 Session().flush()
1000 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
1012 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
1001 # we now calculate the status of pull request, and based on that
1013 # we now calculate the status of pull request, and based on that
1002 # calculation we set the commits status
1014 # calculation we set the commits status
1003 calculated_status = pull_request.calculated_review_status()
1015 calculated_status = pull_request.calculated_review_status()
1004 if old_calculated_status != calculated_status:
1016 if old_calculated_status != calculated_status:
1005 PullRequestModel()._trigger_pull_request_hook(
1017 PullRequestModel()._trigger_pull_request_hook(
1006 pull_request, c.rhodecode_user, 'review_status_change')
1018 pull_request, c.rhodecode_user, 'review_status_change')
1007
1019
1008 calculated_status_lbl = ChangesetStatus.get_status_lbl(
1020 calculated_status_lbl = ChangesetStatus.get_status_lbl(
1009 calculated_status)
1021 calculated_status)
1010
1022
1011 if close_pr:
1023 if close_pr:
1012 status_completed = (
1024 status_completed = (
1013 calculated_status in [ChangesetStatus.STATUS_APPROVED,
1025 calculated_status in [ChangesetStatus.STATUS_APPROVED,
1014 ChangesetStatus.STATUS_REJECTED])
1026 ChangesetStatus.STATUS_REJECTED])
1015 if close_pull_request or status_completed:
1027 if close_pull_request or status_completed:
1016 PullRequestModel().close_pull_request(
1028 PullRequestModel().close_pull_request(
1017 pull_request_id, c.rhodecode_user)
1029 pull_request_id, c.rhodecode_user)
1018 else:
1030 else:
1019 h.flash(_('Closing pull request on other statuses than '
1031 h.flash(_('Closing pull request on other statuses than '
1020 'rejected or approved is forbidden. '
1032 'rejected or approved is forbidden. '
1021 'Calculated status from all reviewers '
1033 'Calculated status from all reviewers '
1022 'is currently: %s') % calculated_status_lbl,
1034 'is currently: %s') % calculated_status_lbl,
1023 category='warning')
1035 category='warning')
1024
1036
1025 Session().commit()
1037 Session().commit()
1026
1038
1027 if not request.is_xhr:
1039 if not request.is_xhr:
1028 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1040 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1029 pull_request_id=pull_request_id))
1041 pull_request_id=pull_request_id))
1030
1042
1031 data = {
1043 data = {
1032 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1044 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1033 }
1045 }
1034 if comm:
1046 if comm:
1035 c.co = comm
1047 c.co = comm
1036 c.inline_comment = True if comm.line_no else False
1048 c.inline_comment = True if comm.line_no else False
1037 data.update(comm.get_dict())
1049 data.update(comm.get_dict())
1038 data.update({'rendered_text':
1050 data.update({'rendered_text':
1039 render('changeset/changeset_comment_block.mako')})
1051 render('changeset/changeset_comment_block.mako')})
1040
1052
1041 return data
1053 return data
1042
1054
1043 @LoginRequired()
1055 @LoginRequired()
1044 @NotAnonymous()
1056 @NotAnonymous()
1045 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1057 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1046 'repository.admin')
1058 'repository.admin')
1047 @auth.CSRFRequired()
1059 @auth.CSRFRequired()
1048 @jsonify
1060 @jsonify
1049 def delete_comment(self, repo_name, comment_id):
1061 def delete_comment(self, repo_name, comment_id):
1050 return self._delete_comment(comment_id)
1062 return self._delete_comment(comment_id)
1051
1063
1052 def _delete_comment(self, comment_id):
1064 def _delete_comment(self, comment_id):
1053 comment_id = safe_int(comment_id)
1065 comment_id = safe_int(comment_id)
1054 co = ChangesetComment.get_or_404(comment_id)
1066 co = ChangesetComment.get_or_404(comment_id)
1055 if co.pull_request.is_closed():
1067 if co.pull_request.is_closed():
1056 # don't allow deleting comments on closed pull request
1068 # don't allow deleting comments on closed pull request
1057 raise HTTPForbidden()
1069 raise HTTPForbidden()
1058
1070
1059 is_owner = co.author.user_id == c.rhodecode_user.user_id
1071 is_owner = co.author.user_id == c.rhodecode_user.user_id
1060 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1072 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1061 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1073 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1062 old_calculated_status = co.pull_request.calculated_review_status()
1074 old_calculated_status = co.pull_request.calculated_review_status()
1063 CommentsModel().delete(comment=co)
1075 CommentsModel().delete(comment=co)
1064 Session().commit()
1076 Session().commit()
1065 calculated_status = co.pull_request.calculated_review_status()
1077 calculated_status = co.pull_request.calculated_review_status()
1066 if old_calculated_status != calculated_status:
1078 if old_calculated_status != calculated_status:
1067 PullRequestModel()._trigger_pull_request_hook(
1079 PullRequestModel()._trigger_pull_request_hook(
1068 co.pull_request, c.rhodecode_user, 'review_status_change')
1080 co.pull_request, c.rhodecode_user, 'review_status_change')
1069 return True
1081 return True
1070 else:
1082 else:
1071 raise HTTPForbidden()
1083 raise HTTPForbidden()
@@ -1,1454 +1,1469 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from sqlalchemy import or_
34 from sqlalchemy import or_
35
35
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
39 from rhodecode.lib.markup_renderer import (
39 from rhodecode.lib.markup_renderer import (
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
41 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils import action_logger
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 CommitDoesNotExistError, EmptyRepositoryError)
47 CommitDoesNotExistError, EmptyRepositoryError)
48 from rhodecode.model import BaseModel
48 from rhodecode.model import BaseModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.db import (
51 from rhodecode.model.db import (
52 PullRequest, PullRequestReviewers, ChangesetStatus,
52 PullRequest, PullRequestReviewers, ChangesetStatus,
53 PullRequestVersion, ChangesetComment, Repository)
53 PullRequestVersion, ChangesetComment, Repository)
54 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
55 from rhodecode.model.notification import NotificationModel, \
55 from rhodecode.model.notification import NotificationModel, \
56 EmailNotificationModel
56 EmailNotificationModel
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.settings import VcsSettingsModel
58 from rhodecode.model.settings import VcsSettingsModel
59
59
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 # Data structure to hold the response data when updating commits during a pull
64 # Data structure to hold the response data when updating commits during a pull
65 # request update.
65 # request update.
66 UpdateResponse = namedtuple(
66 UpdateResponse = namedtuple('UpdateResponse', [
67 'UpdateResponse', 'executed, reason, new, old, changes')
67 'executed', 'reason', 'new', 'old', 'changes',
68 'source_changed', 'target_changed'])
68
69
69
70
70 class PullRequestModel(BaseModel):
71 class PullRequestModel(BaseModel):
71
72
72 cls = PullRequest
73 cls = PullRequest
73
74
74 DIFF_CONTEXT = 3
75 DIFF_CONTEXT = 3
75
76
76 MERGE_STATUS_MESSAGES = {
77 MERGE_STATUS_MESSAGES = {
77 MergeFailureReason.NONE: lazy_ugettext(
78 MergeFailureReason.NONE: lazy_ugettext(
78 'This pull request can be automatically merged.'),
79 'This pull request can be automatically merged.'),
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
80 MergeFailureReason.UNKNOWN: lazy_ugettext(
80 'This pull request cannot be merged because of an unhandled'
81 'This pull request cannot be merged because of an unhandled'
81 ' exception.'),
82 ' exception.'),
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
83 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
83 'This pull request cannot be merged because of merge conflicts.'),
84 'This pull request cannot be merged because of merge conflicts.'),
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
85 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
85 'This pull request could not be merged because push to target'
86 'This pull request could not be merged because push to target'
86 ' failed.'),
87 ' failed.'),
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
88 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
88 'This pull request cannot be merged because the target is not a'
89 'This pull request cannot be merged because the target is not a'
89 ' head.'),
90 ' head.'),
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
91 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
91 'This pull request cannot be merged because the source contains'
92 'This pull request cannot be merged because the source contains'
92 ' more branches than the target.'),
93 ' more branches than the target.'),
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
94 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
94 'This pull request cannot be merged because the target has'
95 'This pull request cannot be merged because the target has'
95 ' multiple heads.'),
96 ' multiple heads.'),
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
97 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
97 'This pull request cannot be merged because the target repository'
98 'This pull request cannot be merged because the target repository'
98 ' is locked.'),
99 ' is locked.'),
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
100 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
100 'This pull request cannot be merged because the target or the '
101 'This pull request cannot be merged because the target or the '
101 'source reference is missing.'),
102 'source reference is missing.'),
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
103 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
103 'This pull request cannot be merged because the target '
104 'This pull request cannot be merged because the target '
104 'reference is missing.'),
105 'reference is missing.'),
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
106 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
106 'This pull request cannot be merged because the source '
107 'This pull request cannot be merged because the source '
107 'reference is missing.'),
108 'reference is missing.'),
108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
109 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
109 'This pull request cannot be merged because of conflicts related '
110 'This pull request cannot be merged because of conflicts related '
110 'to sub repositories.'),
111 'to sub repositories.'),
111 }
112 }
112
113
113 UPDATE_STATUS_MESSAGES = {
114 UPDATE_STATUS_MESSAGES = {
114 UpdateFailureReason.NONE: lazy_ugettext(
115 UpdateFailureReason.NONE: lazy_ugettext(
115 'Pull request update successful.'),
116 'Pull request update successful.'),
116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
117 UpdateFailureReason.UNKNOWN: lazy_ugettext(
117 'Pull request update failed because of an unknown error.'),
118 'Pull request update failed because of an unknown error.'),
118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
119 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
119 'No update needed because the source reference is already '
120 'No update needed because the source and target have not changed.'),
120 'up to date.'),
121 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
121 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
122 'Pull request cannot be updated because the reference type is '
122 'Pull request cannot be updated because the reference type is '
123 'not supported for an update.'),
123 'not supported for an update.'),
124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 'This pull request cannot be updated because the target '
125 'This pull request cannot be updated because the target '
126 'reference is missing.'),
126 'reference is missing.'),
127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 'This pull request cannot be updated because the source '
128 'This pull request cannot be updated because the source '
129 'reference is missing.'),
129 'reference is missing.'),
130 }
130 }
131
131
132 def __get_pull_request(self, pull_request):
132 def __get_pull_request(self, pull_request):
133 return self._get_instance((
133 return self._get_instance((
134 PullRequest, PullRequestVersion), pull_request)
134 PullRequest, PullRequestVersion), pull_request)
135
135
136 def _check_perms(self, perms, pull_request, user, api=False):
136 def _check_perms(self, perms, pull_request, user, api=False):
137 if not api:
137 if not api:
138 return h.HasRepoPermissionAny(*perms)(
138 return h.HasRepoPermissionAny(*perms)(
139 user=user, repo_name=pull_request.target_repo.repo_name)
139 user=user, repo_name=pull_request.target_repo.repo_name)
140 else:
140 else:
141 return h.HasRepoPermissionAnyApi(*perms)(
141 return h.HasRepoPermissionAnyApi(*perms)(
142 user=user, repo_name=pull_request.target_repo.repo_name)
142 user=user, repo_name=pull_request.target_repo.repo_name)
143
143
144 def check_user_read(self, pull_request, user, api=False):
144 def check_user_read(self, pull_request, user, api=False):
145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 return self._check_perms(_perms, pull_request, user, api)
146 return self._check_perms(_perms, pull_request, user, api)
147
147
148 def check_user_merge(self, pull_request, user, api=False):
148 def check_user_merge(self, pull_request, user, api=False):
149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 return self._check_perms(_perms, pull_request, user, api)
150 return self._check_perms(_perms, pull_request, user, api)
151
151
152 def check_user_update(self, pull_request, user, api=False):
152 def check_user_update(self, pull_request, user, api=False):
153 owner = user.user_id == pull_request.user_id
153 owner = user.user_id == pull_request.user_id
154 return self.check_user_merge(pull_request, user, api) or owner
154 return self.check_user_merge(pull_request, user, api) or owner
155
155
156 def check_user_delete(self, pull_request, user):
156 def check_user_delete(self, pull_request, user):
157 owner = user.user_id == pull_request.user_id
157 owner = user.user_id == pull_request.user_id
158 _perms = ('repository.admin',)
158 _perms = ('repository.admin',)
159 return self._check_perms(_perms, pull_request, user) or owner
159 return self._check_perms(_perms, pull_request, user) or owner
160
160
161 def check_user_change_status(self, pull_request, user, api=False):
161 def check_user_change_status(self, pull_request, user, api=False):
162 reviewer = user.user_id in [x.user_id for x in
162 reviewer = user.user_id in [x.user_id for x in
163 pull_request.reviewers]
163 pull_request.reviewers]
164 return self.check_user_update(pull_request, user, api) or reviewer
164 return self.check_user_update(pull_request, user, api) or reviewer
165
165
166 def get(self, pull_request):
166 def get(self, pull_request):
167 return self.__get_pull_request(pull_request)
167 return self.__get_pull_request(pull_request)
168
168
169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
170 opened_by=None, order_by=None,
170 opened_by=None, order_by=None,
171 order_dir='desc'):
171 order_dir='desc'):
172 repo = None
172 repo = None
173 if repo_name:
173 if repo_name:
174 repo = self._get_repo(repo_name)
174 repo = self._get_repo(repo_name)
175
175
176 q = PullRequest.query()
176 q = PullRequest.query()
177
177
178 # source or target
178 # source or target
179 if repo and source:
179 if repo and source:
180 q = q.filter(PullRequest.source_repo == repo)
180 q = q.filter(PullRequest.source_repo == repo)
181 elif repo:
181 elif repo:
182 q = q.filter(PullRequest.target_repo == repo)
182 q = q.filter(PullRequest.target_repo == repo)
183
183
184 # closed,opened
184 # closed,opened
185 if statuses:
185 if statuses:
186 q = q.filter(PullRequest.status.in_(statuses))
186 q = q.filter(PullRequest.status.in_(statuses))
187
187
188 # opened by filter
188 # opened by filter
189 if opened_by:
189 if opened_by:
190 q = q.filter(PullRequest.user_id.in_(opened_by))
190 q = q.filter(PullRequest.user_id.in_(opened_by))
191
191
192 if order_by:
192 if order_by:
193 order_map = {
193 order_map = {
194 'name_raw': PullRequest.pull_request_id,
194 'name_raw': PullRequest.pull_request_id,
195 'title': PullRequest.title,
195 'title': PullRequest.title,
196 'updated_on_raw': PullRequest.updated_on,
196 'updated_on_raw': PullRequest.updated_on,
197 'target_repo': PullRequest.target_repo_id
197 'target_repo': PullRequest.target_repo_id
198 }
198 }
199 if order_dir == 'asc':
199 if order_dir == 'asc':
200 q = q.order_by(order_map[order_by].asc())
200 q = q.order_by(order_map[order_by].asc())
201 else:
201 else:
202 q = q.order_by(order_map[order_by].desc())
202 q = q.order_by(order_map[order_by].desc())
203
203
204 return q
204 return q
205
205
206 def count_all(self, repo_name, source=False, statuses=None,
206 def count_all(self, repo_name, source=False, statuses=None,
207 opened_by=None):
207 opened_by=None):
208 """
208 """
209 Count the number of pull requests for a specific repository.
209 Count the number of pull requests for a specific repository.
210
210
211 :param repo_name: target or source repo
211 :param repo_name: target or source repo
212 :param source: boolean flag to specify if repo_name refers to source
212 :param source: boolean flag to specify if repo_name refers to source
213 :param statuses: list of pull request statuses
213 :param statuses: list of pull request statuses
214 :param opened_by: author user of the pull request
214 :param opened_by: author user of the pull request
215 :returns: int number of pull requests
215 :returns: int number of pull requests
216 """
216 """
217 q = self._prepare_get_all_query(
217 q = self._prepare_get_all_query(
218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
219
219
220 return q.count()
220 return q.count()
221
221
222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
223 offset=0, length=None, order_by=None, order_dir='desc'):
223 offset=0, length=None, order_by=None, order_dir='desc'):
224 """
224 """
225 Get all pull requests for a specific repository.
225 Get all pull requests for a specific repository.
226
226
227 :param repo_name: target or source repo
227 :param repo_name: target or source repo
228 :param source: boolean flag to specify if repo_name refers to source
228 :param source: boolean flag to specify if repo_name refers to source
229 :param statuses: list of pull request statuses
229 :param statuses: list of pull request statuses
230 :param opened_by: author user of the pull request
230 :param opened_by: author user of the pull request
231 :param offset: pagination offset
231 :param offset: pagination offset
232 :param length: length of returned list
232 :param length: length of returned list
233 :param order_by: order of the returned list
233 :param order_by: order of the returned list
234 :param order_dir: 'asc' or 'desc' ordering direction
234 :param order_dir: 'asc' or 'desc' ordering direction
235 :returns: list of pull requests
235 :returns: list of pull requests
236 """
236 """
237 q = self._prepare_get_all_query(
237 q = self._prepare_get_all_query(
238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
239 order_by=order_by, order_dir=order_dir)
239 order_by=order_by, order_dir=order_dir)
240
240
241 if length:
241 if length:
242 pull_requests = q.limit(length).offset(offset).all()
242 pull_requests = q.limit(length).offset(offset).all()
243 else:
243 else:
244 pull_requests = q.all()
244 pull_requests = q.all()
245
245
246 return pull_requests
246 return pull_requests
247
247
248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
249 opened_by=None):
249 opened_by=None):
250 """
250 """
251 Count the number of pull requests for a specific repository that are
251 Count the number of pull requests for a specific repository that are
252 awaiting review.
252 awaiting review.
253
253
254 :param repo_name: target or source repo
254 :param repo_name: target or source repo
255 :param source: boolean flag to specify if repo_name refers to source
255 :param source: boolean flag to specify if repo_name refers to source
256 :param statuses: list of pull request statuses
256 :param statuses: list of pull request statuses
257 :param opened_by: author user of the pull request
257 :param opened_by: author user of the pull request
258 :returns: int number of pull requests
258 :returns: int number of pull requests
259 """
259 """
260 pull_requests = self.get_awaiting_review(
260 pull_requests = self.get_awaiting_review(
261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
262
262
263 return len(pull_requests)
263 return len(pull_requests)
264
264
265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
266 opened_by=None, offset=0, length=None,
266 opened_by=None, offset=0, length=None,
267 order_by=None, order_dir='desc'):
267 order_by=None, order_dir='desc'):
268 """
268 """
269 Get all pull requests for a specific repository that are awaiting
269 Get all pull requests for a specific repository that are awaiting
270 review.
270 review.
271
271
272 :param repo_name: target or source repo
272 :param repo_name: target or source repo
273 :param source: boolean flag to specify if repo_name refers to source
273 :param source: boolean flag to specify if repo_name refers to source
274 :param statuses: list of pull request statuses
274 :param statuses: list of pull request statuses
275 :param opened_by: author user of the pull request
275 :param opened_by: author user of the pull request
276 :param offset: pagination offset
276 :param offset: pagination offset
277 :param length: length of returned list
277 :param length: length of returned list
278 :param order_by: order of the returned list
278 :param order_by: order of the returned list
279 :param order_dir: 'asc' or 'desc' ordering direction
279 :param order_dir: 'asc' or 'desc' ordering direction
280 :returns: list of pull requests
280 :returns: list of pull requests
281 """
281 """
282 pull_requests = self.get_all(
282 pull_requests = self.get_all(
283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
284 order_by=order_by, order_dir=order_dir)
284 order_by=order_by, order_dir=order_dir)
285
285
286 _filtered_pull_requests = []
286 _filtered_pull_requests = []
287 for pr in pull_requests:
287 for pr in pull_requests:
288 status = pr.calculated_review_status()
288 status = pr.calculated_review_status()
289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
290 ChangesetStatus.STATUS_UNDER_REVIEW]:
290 ChangesetStatus.STATUS_UNDER_REVIEW]:
291 _filtered_pull_requests.append(pr)
291 _filtered_pull_requests.append(pr)
292 if length:
292 if length:
293 return _filtered_pull_requests[offset:offset+length]
293 return _filtered_pull_requests[offset:offset+length]
294 else:
294 else:
295 return _filtered_pull_requests
295 return _filtered_pull_requests
296
296
297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
298 opened_by=None, user_id=None):
298 opened_by=None, user_id=None):
299 """
299 """
300 Count the number of pull requests for a specific repository that are
300 Count the number of pull requests for a specific repository that are
301 awaiting review from a specific user.
301 awaiting review from a specific user.
302
302
303 :param repo_name: target or source repo
303 :param repo_name: target or source repo
304 :param source: boolean flag to specify if repo_name refers to source
304 :param source: boolean flag to specify if repo_name refers to source
305 :param statuses: list of pull request statuses
305 :param statuses: list of pull request statuses
306 :param opened_by: author user of the pull request
306 :param opened_by: author user of the pull request
307 :param user_id: reviewer user of the pull request
307 :param user_id: reviewer user of the pull request
308 :returns: int number of pull requests
308 :returns: int number of pull requests
309 """
309 """
310 pull_requests = self.get_awaiting_my_review(
310 pull_requests = self.get_awaiting_my_review(
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 user_id=user_id)
312 user_id=user_id)
313
313
314 return len(pull_requests)
314 return len(pull_requests)
315
315
316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
317 opened_by=None, user_id=None, offset=0,
317 opened_by=None, user_id=None, offset=0,
318 length=None, order_by=None, order_dir='desc'):
318 length=None, order_by=None, order_dir='desc'):
319 """
319 """
320 Get all pull requests for a specific repository that are awaiting
320 Get all pull requests for a specific repository that are awaiting
321 review from a specific user.
321 review from a specific user.
322
322
323 :param repo_name: target or source repo
323 :param repo_name: target or source repo
324 :param source: boolean flag to specify if repo_name refers to source
324 :param source: boolean flag to specify if repo_name refers to source
325 :param statuses: list of pull request statuses
325 :param statuses: list of pull request statuses
326 :param opened_by: author user of the pull request
326 :param opened_by: author user of the pull request
327 :param user_id: reviewer user of the pull request
327 :param user_id: reviewer user of the pull request
328 :param offset: pagination offset
328 :param offset: pagination offset
329 :param length: length of returned list
329 :param length: length of returned list
330 :param order_by: order of the returned list
330 :param order_by: order of the returned list
331 :param order_dir: 'asc' or 'desc' ordering direction
331 :param order_dir: 'asc' or 'desc' ordering direction
332 :returns: list of pull requests
332 :returns: list of pull requests
333 """
333 """
334 pull_requests = self.get_all(
334 pull_requests = self.get_all(
335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
336 order_by=order_by, order_dir=order_dir)
336 order_by=order_by, order_dir=order_dir)
337
337
338 _my = PullRequestModel().get_not_reviewed(user_id)
338 _my = PullRequestModel().get_not_reviewed(user_id)
339 my_participation = []
339 my_participation = []
340 for pr in pull_requests:
340 for pr in pull_requests:
341 if pr in _my:
341 if pr in _my:
342 my_participation.append(pr)
342 my_participation.append(pr)
343 _filtered_pull_requests = my_participation
343 _filtered_pull_requests = my_participation
344 if length:
344 if length:
345 return _filtered_pull_requests[offset:offset+length]
345 return _filtered_pull_requests[offset:offset+length]
346 else:
346 else:
347 return _filtered_pull_requests
347 return _filtered_pull_requests
348
348
349 def get_not_reviewed(self, user_id):
349 def get_not_reviewed(self, user_id):
350 return [
350 return [
351 x.pull_request for x in PullRequestReviewers.query().filter(
351 x.pull_request for x in PullRequestReviewers.query().filter(
352 PullRequestReviewers.user_id == user_id).all()
352 PullRequestReviewers.user_id == user_id).all()
353 ]
353 ]
354
354
355 def _prepare_participating_query(self, user_id=None, statuses=None,
355 def _prepare_participating_query(self, user_id=None, statuses=None,
356 order_by=None, order_dir='desc'):
356 order_by=None, order_dir='desc'):
357 q = PullRequest.query()
357 q = PullRequest.query()
358 if user_id:
358 if user_id:
359 reviewers_subquery = Session().query(
359 reviewers_subquery = Session().query(
360 PullRequestReviewers.pull_request_id).filter(
360 PullRequestReviewers.pull_request_id).filter(
361 PullRequestReviewers.user_id == user_id).subquery()
361 PullRequestReviewers.user_id == user_id).subquery()
362 user_filter= or_(
362 user_filter= or_(
363 PullRequest.user_id == user_id,
363 PullRequest.user_id == user_id,
364 PullRequest.pull_request_id.in_(reviewers_subquery)
364 PullRequest.pull_request_id.in_(reviewers_subquery)
365 )
365 )
366 q = PullRequest.query().filter(user_filter)
366 q = PullRequest.query().filter(user_filter)
367
367
368 # closed,opened
368 # closed,opened
369 if statuses:
369 if statuses:
370 q = q.filter(PullRequest.status.in_(statuses))
370 q = q.filter(PullRequest.status.in_(statuses))
371
371
372 if order_by:
372 if order_by:
373 order_map = {
373 order_map = {
374 'name_raw': PullRequest.pull_request_id,
374 'name_raw': PullRequest.pull_request_id,
375 'title': PullRequest.title,
375 'title': PullRequest.title,
376 'updated_on_raw': PullRequest.updated_on,
376 'updated_on_raw': PullRequest.updated_on,
377 'target_repo': PullRequest.target_repo_id
377 'target_repo': PullRequest.target_repo_id
378 }
378 }
379 if order_dir == 'asc':
379 if order_dir == 'asc':
380 q = q.order_by(order_map[order_by].asc())
380 q = q.order_by(order_map[order_by].asc())
381 else:
381 else:
382 q = q.order_by(order_map[order_by].desc())
382 q = q.order_by(order_map[order_by].desc())
383
383
384 return q
384 return q
385
385
386 def count_im_participating_in(self, user_id=None, statuses=None):
386 def count_im_participating_in(self, user_id=None, statuses=None):
387 q = self._prepare_participating_query(user_id, statuses=statuses)
387 q = self._prepare_participating_query(user_id, statuses=statuses)
388 return q.count()
388 return q.count()
389
389
390 def get_im_participating_in(
390 def get_im_participating_in(
391 self, user_id=None, statuses=None, offset=0,
391 self, user_id=None, statuses=None, offset=0,
392 length=None, order_by=None, order_dir='desc'):
392 length=None, order_by=None, order_dir='desc'):
393 """
393 """
394 Get all Pull requests that i'm participating in, or i have opened
394 Get all Pull requests that i'm participating in, or i have opened
395 """
395 """
396
396
397 q = self._prepare_participating_query(
397 q = self._prepare_participating_query(
398 user_id, statuses=statuses, order_by=order_by,
398 user_id, statuses=statuses, order_by=order_by,
399 order_dir=order_dir)
399 order_dir=order_dir)
400
400
401 if length:
401 if length:
402 pull_requests = q.limit(length).offset(offset).all()
402 pull_requests = q.limit(length).offset(offset).all()
403 else:
403 else:
404 pull_requests = q.all()
404 pull_requests = q.all()
405
405
406 return pull_requests
406 return pull_requests
407
407
408 def get_versions(self, pull_request):
408 def get_versions(self, pull_request):
409 """
409 """
410 returns version of pull request sorted by ID descending
410 returns version of pull request sorted by ID descending
411 """
411 """
412 return PullRequestVersion.query()\
412 return PullRequestVersion.query()\
413 .filter(PullRequestVersion.pull_request == pull_request)\
413 .filter(PullRequestVersion.pull_request == pull_request)\
414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
415 .all()
415 .all()
416
416
417 def create(self, created_by, source_repo, source_ref, target_repo,
417 def create(self, created_by, source_repo, source_ref, target_repo,
418 target_ref, revisions, reviewers, title, description=None):
418 target_ref, revisions, reviewers, title, description=None):
419 created_by_user = self._get_user(created_by)
419 created_by_user = self._get_user(created_by)
420 source_repo = self._get_repo(source_repo)
420 source_repo = self._get_repo(source_repo)
421 target_repo = self._get_repo(target_repo)
421 target_repo = self._get_repo(target_repo)
422
422
423 pull_request = PullRequest()
423 pull_request = PullRequest()
424 pull_request.source_repo = source_repo
424 pull_request.source_repo = source_repo
425 pull_request.source_ref = source_ref
425 pull_request.source_ref = source_ref
426 pull_request.target_repo = target_repo
426 pull_request.target_repo = target_repo
427 pull_request.target_ref = target_ref
427 pull_request.target_ref = target_ref
428 pull_request.revisions = revisions
428 pull_request.revisions = revisions
429 pull_request.title = title
429 pull_request.title = title
430 pull_request.description = description
430 pull_request.description = description
431 pull_request.author = created_by_user
431 pull_request.author = created_by_user
432
432
433 Session().add(pull_request)
433 Session().add(pull_request)
434 Session().flush()
434 Session().flush()
435
435
436 reviewer_ids = set()
436 reviewer_ids = set()
437 # members / reviewers
437 # members / reviewers
438 for reviewer_object in reviewers:
438 for reviewer_object in reviewers:
439 if isinstance(reviewer_object, tuple):
439 if isinstance(reviewer_object, tuple):
440 user_id, reasons = reviewer_object
440 user_id, reasons = reviewer_object
441 else:
441 else:
442 user_id, reasons = reviewer_object, []
442 user_id, reasons = reviewer_object, []
443
443
444 user = self._get_user(user_id)
444 user = self._get_user(user_id)
445 reviewer_ids.add(user.user_id)
445 reviewer_ids.add(user.user_id)
446
446
447 reviewer = PullRequestReviewers(user, pull_request, reasons)
447 reviewer = PullRequestReviewers(user, pull_request, reasons)
448 Session().add(reviewer)
448 Session().add(reviewer)
449
449
450 # Set approval status to "Under Review" for all commits which are
450 # Set approval status to "Under Review" for all commits which are
451 # part of this pull request.
451 # part of this pull request.
452 ChangesetStatusModel().set_status(
452 ChangesetStatusModel().set_status(
453 repo=target_repo,
453 repo=target_repo,
454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
455 user=created_by_user,
455 user=created_by_user,
456 pull_request=pull_request
456 pull_request=pull_request
457 )
457 )
458
458
459 self.notify_reviewers(pull_request, reviewer_ids)
459 self.notify_reviewers(pull_request, reviewer_ids)
460 self._trigger_pull_request_hook(
460 self._trigger_pull_request_hook(
461 pull_request, created_by_user, 'create')
461 pull_request, created_by_user, 'create')
462
462
463 return pull_request
463 return pull_request
464
464
465 def _trigger_pull_request_hook(self, pull_request, user, action):
465 def _trigger_pull_request_hook(self, pull_request, user, action):
466 pull_request = self.__get_pull_request(pull_request)
466 pull_request = self.__get_pull_request(pull_request)
467 target_scm = pull_request.target_repo.scm_instance()
467 target_scm = pull_request.target_repo.scm_instance()
468 if action == 'create':
468 if action == 'create':
469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
470 elif action == 'merge':
470 elif action == 'merge':
471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
472 elif action == 'close':
472 elif action == 'close':
473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
474 elif action == 'review_status_change':
474 elif action == 'review_status_change':
475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
476 elif action == 'update':
476 elif action == 'update':
477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
478 else:
478 else:
479 return
479 return
480
480
481 trigger_hook(
481 trigger_hook(
482 username=user.username,
482 username=user.username,
483 repo_name=pull_request.target_repo.repo_name,
483 repo_name=pull_request.target_repo.repo_name,
484 repo_alias=target_scm.alias,
484 repo_alias=target_scm.alias,
485 pull_request=pull_request)
485 pull_request=pull_request)
486
486
487 def _get_commit_ids(self, pull_request):
487 def _get_commit_ids(self, pull_request):
488 """
488 """
489 Return the commit ids of the merged pull request.
489 Return the commit ids of the merged pull request.
490
490
491 This method is not dealing correctly yet with the lack of autoupdates
491 This method is not dealing correctly yet with the lack of autoupdates
492 nor with the implicit target updates.
492 nor with the implicit target updates.
493 For example: if a commit in the source repo is already in the target it
493 For example: if a commit in the source repo is already in the target it
494 will be reported anyways.
494 will be reported anyways.
495 """
495 """
496 merge_rev = pull_request.merge_rev
496 merge_rev = pull_request.merge_rev
497 if merge_rev is None:
497 if merge_rev is None:
498 raise ValueError('This pull request was not merged yet')
498 raise ValueError('This pull request was not merged yet')
499
499
500 commit_ids = list(pull_request.revisions)
500 commit_ids = list(pull_request.revisions)
501 if merge_rev not in commit_ids:
501 if merge_rev not in commit_ids:
502 commit_ids.append(merge_rev)
502 commit_ids.append(merge_rev)
503
503
504 return commit_ids
504 return commit_ids
505
505
506 def merge(self, pull_request, user, extras):
506 def merge(self, pull_request, user, extras):
507 log.debug("Merging pull request %s", pull_request.pull_request_id)
507 log.debug("Merging pull request %s", pull_request.pull_request_id)
508 merge_state = self._merge_pull_request(pull_request, user, extras)
508 merge_state = self._merge_pull_request(pull_request, user, extras)
509 if merge_state.executed:
509 if merge_state.executed:
510 log.debug(
510 log.debug(
511 "Merge was successful, updating the pull request comments.")
511 "Merge was successful, updating the pull request comments.")
512 self._comment_and_close_pr(pull_request, user, merge_state)
512 self._comment_and_close_pr(pull_request, user, merge_state)
513 self._log_action('user_merged_pull_request', user, pull_request)
513 self._log_action('user_merged_pull_request', user, pull_request)
514 else:
514 else:
515 log.warn("Merge failed, not updating the pull request.")
515 log.warn("Merge failed, not updating the pull request.")
516 return merge_state
516 return merge_state
517
517
518 def _merge_pull_request(self, pull_request, user, extras):
518 def _merge_pull_request(self, pull_request, user, extras):
519 target_vcs = pull_request.target_repo.scm_instance()
519 target_vcs = pull_request.target_repo.scm_instance()
520 source_vcs = pull_request.source_repo.scm_instance()
520 source_vcs = pull_request.source_repo.scm_instance()
521 target_ref = self._refresh_reference(
521 target_ref = self._refresh_reference(
522 pull_request.target_ref_parts, target_vcs)
522 pull_request.target_ref_parts, target_vcs)
523
523
524 message = _(
524 message = _(
525 'Merge pull request #%(pr_id)s from '
525 'Merge pull request #%(pr_id)s from '
526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
527 'pr_id': pull_request.pull_request_id,
527 'pr_id': pull_request.pull_request_id,
528 'source_repo': source_vcs.name,
528 'source_repo': source_vcs.name,
529 'source_ref_name': pull_request.source_ref_parts.name,
529 'source_ref_name': pull_request.source_ref_parts.name,
530 'pr_title': pull_request.title
530 'pr_title': pull_request.title
531 }
531 }
532
532
533 workspace_id = self._workspace_id(pull_request)
533 workspace_id = self._workspace_id(pull_request)
534 use_rebase = self._use_rebase_for_merging(pull_request)
534 use_rebase = self._use_rebase_for_merging(pull_request)
535
535
536 callback_daemon, extras = prepare_callback_daemon(
536 callback_daemon, extras = prepare_callback_daemon(
537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
539
539
540 with callback_daemon:
540 with callback_daemon:
541 # TODO: johbo: Implement a clean way to run a config_override
541 # TODO: johbo: Implement a clean way to run a config_override
542 # for a single call.
542 # for a single call.
543 target_vcs.config.set(
543 target_vcs.config.set(
544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
545 merge_state = target_vcs.merge(
545 merge_state = target_vcs.merge(
546 target_ref, source_vcs, pull_request.source_ref_parts,
546 target_ref, source_vcs, pull_request.source_ref_parts,
547 workspace_id, user_name=user.username,
547 workspace_id, user_name=user.username,
548 user_email=user.email, message=message, use_rebase=use_rebase)
548 user_email=user.email, message=message, use_rebase=use_rebase)
549 return merge_state
549 return merge_state
550
550
551 def _comment_and_close_pr(self, pull_request, user, merge_state):
551 def _comment_and_close_pr(self, pull_request, user, merge_state):
552 pull_request.merge_rev = merge_state.merge_ref.commit_id
552 pull_request.merge_rev = merge_state.merge_ref.commit_id
553 pull_request.updated_on = datetime.datetime.now()
553 pull_request.updated_on = datetime.datetime.now()
554
554
555 CommentsModel().create(
555 CommentsModel().create(
556 text=unicode(_('Pull request merged and closed')),
556 text=unicode(_('Pull request merged and closed')),
557 repo=pull_request.target_repo.repo_id,
557 repo=pull_request.target_repo.repo_id,
558 user=user.user_id,
558 user=user.user_id,
559 pull_request=pull_request.pull_request_id,
559 pull_request=pull_request.pull_request_id,
560 f_path=None,
560 f_path=None,
561 line_no=None,
561 line_no=None,
562 closing_pr=True
562 closing_pr=True
563 )
563 )
564
564
565 Session().add(pull_request)
565 Session().add(pull_request)
566 Session().flush()
566 Session().flush()
567 # TODO: paris: replace invalidation with less radical solution
567 # TODO: paris: replace invalidation with less radical solution
568 ScmModel().mark_for_invalidation(
568 ScmModel().mark_for_invalidation(
569 pull_request.target_repo.repo_name)
569 pull_request.target_repo.repo_name)
570 self._trigger_pull_request_hook(pull_request, user, 'merge')
570 self._trigger_pull_request_hook(pull_request, user, 'merge')
571
571
572 def has_valid_update_type(self, pull_request):
572 def has_valid_update_type(self, pull_request):
573 source_ref_type = pull_request.source_ref_parts.type
573 source_ref_type = pull_request.source_ref_parts.type
574 return source_ref_type in ['book', 'branch', 'tag']
574 return source_ref_type in ['book', 'branch', 'tag']
575
575
576 def update_commits(self, pull_request):
576 def update_commits(self, pull_request):
577 """
577 """
578 Get the updated list of commits for the pull request
578 Get the updated list of commits for the pull request
579 and return the new pull request version and the list
579 and return the new pull request version and the list
580 of commits processed by this update action
580 of commits processed by this update action
581 """
581 """
582 pull_request = self.__get_pull_request(pull_request)
582 pull_request = self.__get_pull_request(pull_request)
583 source_ref_type = pull_request.source_ref_parts.type
583 source_ref_type = pull_request.source_ref_parts.type
584 source_ref_name = pull_request.source_ref_parts.name
584 source_ref_name = pull_request.source_ref_parts.name
585 source_ref_id = pull_request.source_ref_parts.commit_id
585 source_ref_id = pull_request.source_ref_parts.commit_id
586
586
587 target_ref_type = pull_request.target_ref_parts.type
587 target_ref_type = pull_request.target_ref_parts.type
588 target_ref_name = pull_request.target_ref_parts.name
588 target_ref_name = pull_request.target_ref_parts.name
589 target_ref_id = pull_request.target_ref_parts.commit_id
589 target_ref_id = pull_request.target_ref_parts.commit_id
590
590
591 if not self.has_valid_update_type(pull_request):
591 if not self.has_valid_update_type(pull_request):
592 log.debug(
592 log.debug(
593 "Skipping update of pull request %s due to ref type: %s",
593 "Skipping update of pull request %s due to ref type: %s",
594 pull_request, source_ref_type)
594 pull_request, source_ref_type)
595 return UpdateResponse(
595 return UpdateResponse(
596 executed=False,
596 executed=False,
597 reason=UpdateFailureReason.WRONG_REF_TPYE,
597 reason=UpdateFailureReason.WRONG_REF_TPYE,
598 old=pull_request, new=None, changes=None)
598 old=pull_request, new=None, changes=None,
599 source_changed=False, target_changed=False)
599
600
600 # source repo
601 # source repo
601 source_repo = pull_request.source_repo.scm_instance()
602 source_repo = pull_request.source_repo.scm_instance()
602 try:
603 try:
603 source_commit = source_repo.get_commit(commit_id=source_ref_name)
604 source_commit = source_repo.get_commit(commit_id=source_ref_name)
604 except CommitDoesNotExistError:
605 except CommitDoesNotExistError:
605 return UpdateResponse(
606 return UpdateResponse(
606 executed=False,
607 executed=False,
607 reason=UpdateFailureReason.MISSING_SOURCE_REF,
608 reason=UpdateFailureReason.MISSING_SOURCE_REF,
608 old=pull_request, new=None, changes=None)
609 old=pull_request, new=None, changes=None,
610 source_changed=False, target_changed=False)
609
611
610 source_changed = source_ref_id != source_commit.raw_id
612 source_changed = source_ref_id != source_commit.raw_id
611
613
612 # target repo
614 # target repo
613 target_repo = pull_request.target_repo.scm_instance()
615 target_repo = pull_request.target_repo.scm_instance()
614 try:
616 try:
615 target_commit = target_repo.get_commit(commit_id=target_ref_name)
617 target_commit = target_repo.get_commit(commit_id=target_ref_name)
616 except CommitDoesNotExistError:
618 except CommitDoesNotExistError:
617 return UpdateResponse(
619 return UpdateResponse(
618 executed=False,
620 executed=False,
619 reason=UpdateFailureReason.MISSING_TARGET_REF,
621 reason=UpdateFailureReason.MISSING_TARGET_REF,
620 old=pull_request, new=None, changes=None)
622 old=pull_request, new=None, changes=None,
623 source_changed=False, target_changed=False)
621 target_changed = target_ref_id != target_commit.raw_id
624 target_changed = target_ref_id != target_commit.raw_id
622
625
623 if not (source_changed or target_changed):
626 if not (source_changed or target_changed):
624 log.debug("Nothing changed in pull request %s", pull_request)
627 log.debug("Nothing changed in pull request %s", pull_request)
625 return UpdateResponse(
628 return UpdateResponse(
626 executed=False,
629 executed=False,
627 reason=UpdateFailureReason.NO_CHANGE,
630 reason=UpdateFailureReason.NO_CHANGE,
628 old=pull_request, new=None, changes=None)
631 old=pull_request, new=None, changes=None,
632 source_changed=target_changed, target_changed=source_changed)
629
633
630 change_in_found = 'target repo' if target_changed else 'source repo'
634 change_in_found = 'target repo' if target_changed else 'source repo'
631 log.debug('Updating pull request because of change in %s detected',
635 log.debug('Updating pull request because of change in %s detected',
632 change_in_found)
636 change_in_found)
633
637
634 # Finally there is a need for an update, in case of source change
638 # Finally there is a need for an update, in case of source change
635 # we create a new version, else just an update
639 # we create a new version, else just an update
636 if source_changed:
640 if source_changed:
637 pull_request_version = self._create_version_from_snapshot(pull_request)
641 pull_request_version = self._create_version_from_snapshot(pull_request)
638 self._link_comments_to_version(pull_request_version)
642 self._link_comments_to_version(pull_request_version)
639 else:
643 else:
640 try:
644 try:
641 ver = pull_request.versions[-1]
645 ver = pull_request.versions[-1]
642 except IndexError:
646 except IndexError:
643 ver = None
647 ver = None
644
648
645 pull_request.pull_request_version_id = \
649 pull_request.pull_request_version_id = \
646 ver.pull_request_version_id if ver else None
650 ver.pull_request_version_id if ver else None
647 pull_request_version = pull_request
651 pull_request_version = pull_request
648
652
649 try:
653 try:
650 if target_ref_type in ('tag', 'branch', 'book'):
654 if target_ref_type in ('tag', 'branch', 'book'):
651 target_commit = target_repo.get_commit(target_ref_name)
655 target_commit = target_repo.get_commit(target_ref_name)
652 else:
656 else:
653 target_commit = target_repo.get_commit(target_ref_id)
657 target_commit = target_repo.get_commit(target_ref_id)
654 except CommitDoesNotExistError:
658 except CommitDoesNotExistError:
655 return UpdateResponse(
659 return UpdateResponse(
656 executed=False,
660 executed=False,
657 reason=UpdateFailureReason.MISSING_TARGET_REF,
661 reason=UpdateFailureReason.MISSING_TARGET_REF,
658 old=pull_request, new=None, changes=None)
662 old=pull_request, new=None, changes=None,
663 source_changed=source_changed, target_changed=target_changed)
659
664
660 # re-compute commit ids
665 # re-compute commit ids
661 old_commit_ids = pull_request.revisions
666 old_commit_ids = pull_request.revisions
662 pre_load = ["author", "branch", "date", "message"]
667 pre_load = ["author", "branch", "date", "message"]
663 commit_ranges = target_repo.compare(
668 commit_ranges = target_repo.compare(
664 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
669 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
665 pre_load=pre_load)
670 pre_load=pre_load)
666
671
667 ancestor = target_repo.get_common_ancestor(
672 ancestor = target_repo.get_common_ancestor(
668 target_commit.raw_id, source_commit.raw_id, source_repo)
673 target_commit.raw_id, source_commit.raw_id, source_repo)
669
674
670 pull_request.source_ref = '%s:%s:%s' % (
675 pull_request.source_ref = '%s:%s:%s' % (
671 source_ref_type, source_ref_name, source_commit.raw_id)
676 source_ref_type, source_ref_name, source_commit.raw_id)
672 pull_request.target_ref = '%s:%s:%s' % (
677 pull_request.target_ref = '%s:%s:%s' % (
673 target_ref_type, target_ref_name, ancestor)
678 target_ref_type, target_ref_name, ancestor)
674
679
675 pull_request.revisions = [
680 pull_request.revisions = [
676 commit.raw_id for commit in reversed(commit_ranges)]
681 commit.raw_id for commit in reversed(commit_ranges)]
677 pull_request.updated_on = datetime.datetime.now()
682 pull_request.updated_on = datetime.datetime.now()
678 Session().add(pull_request)
683 Session().add(pull_request)
679 new_commit_ids = pull_request.revisions
684 new_commit_ids = pull_request.revisions
680
685
681 changes = self._calculate_commit_id_changes(
682 old_commit_ids, new_commit_ids)
683
684 old_diff_data, new_diff_data = self._generate_update_diffs(
686 old_diff_data, new_diff_data = self._generate_update_diffs(
685 pull_request, pull_request_version)
687 pull_request, pull_request_version)
686
688
689 # calculate commit and file changes
690 changes = self._calculate_commit_id_changes(
691 old_commit_ids, new_commit_ids)
692 file_changes = self._calculate_file_changes(
693 old_diff_data, new_diff_data)
694
695 # set comments as outdated if DIFFS changed
687 CommentsModel().outdate_comments(
696 CommentsModel().outdate_comments(
688 pull_request, old_diff_data=old_diff_data,
697 pull_request, old_diff_data=old_diff_data,
689 new_diff_data=new_diff_data)
698 new_diff_data=new_diff_data)
690
699
691 file_changes = self._calculate_file_changes(
700 commit_changes = (changes.added or changes.removed)
692 old_diff_data, new_diff_data)
701 file_node_changes = (
702 file_changes.added or file_changes.modified or file_changes.removed)
703 pr_has_changes = commit_changes or file_node_changes
693
704
694 # Add an automatic comment to the pull request
705 # Add an automatic comment to the pull request, in case
695 update_comment = CommentsModel().create(
706 # anything has changed
696 text=self._render_update_message(changes, file_changes),
707 if pr_has_changes:
697 repo=pull_request.target_repo,
708 update_comment = CommentsModel().create(
698 user=pull_request.author,
709 text=self._render_update_message(changes, file_changes),
699 pull_request=pull_request,
710 repo=pull_request.target_repo,
700 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
701
702 # Update status to "Under Review" for added commits
703 for commit_id in changes.added:
704 ChangesetStatusModel().set_status(
705 repo=pull_request.source_repo,
706 status=ChangesetStatus.STATUS_UNDER_REVIEW,
707 comment=update_comment,
708 user=pull_request.author,
711 user=pull_request.author,
709 pull_request=pull_request,
712 pull_request=pull_request,
710 revision=commit_id)
713 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
714
715 # Update status to "Under Review" for added commits
716 for commit_id in changes.added:
717 ChangesetStatusModel().set_status(
718 repo=pull_request.source_repo,
719 status=ChangesetStatus.STATUS_UNDER_REVIEW,
720 comment=update_comment,
721 user=pull_request.author,
722 pull_request=pull_request,
723 revision=commit_id)
711
724
712 log.debug(
725 log.debug(
713 'Updated pull request %s, added_ids: %s, common_ids: %s, '
726 'Updated pull request %s, added_ids: %s, common_ids: %s, '
714 'removed_ids: %s', pull_request.pull_request_id,
727 'removed_ids: %s', pull_request.pull_request_id,
715 changes.added, changes.common, changes.removed)
728 changes.added, changes.common, changes.removed)
716 log.debug('Updated pull request with the following file changes: %s',
729 log.debug(
717 file_changes)
730 'Updated pull request with the following file changes: %s',
731 file_changes)
718
732
719 log.info(
733 log.info(
720 "Updated pull request %s from commit %s to commit %s, "
734 "Updated pull request %s from commit %s to commit %s, "
721 "stored new version %s of this pull request.",
735 "stored new version %s of this pull request.",
722 pull_request.pull_request_id, source_ref_id,
736 pull_request.pull_request_id, source_ref_id,
723 pull_request.source_ref_parts.commit_id,
737 pull_request.source_ref_parts.commit_id,
724 pull_request_version.pull_request_version_id)
738 pull_request_version.pull_request_version_id)
725 Session().commit()
739 Session().commit()
726 self._trigger_pull_request_hook(pull_request, pull_request.author,
740 self._trigger_pull_request_hook(
727 'update')
741 pull_request, pull_request.author, 'update')
728
742
729 return UpdateResponse(
743 return UpdateResponse(
730 executed=True, reason=UpdateFailureReason.NONE,
744 executed=True, reason=UpdateFailureReason.NONE,
731 old=pull_request, new=pull_request_version, changes=changes)
745 old=pull_request, new=pull_request_version, changes=changes,
746 source_changed=source_changed, target_changed=target_changed)
732
747
733 def _create_version_from_snapshot(self, pull_request):
748 def _create_version_from_snapshot(self, pull_request):
734 version = PullRequestVersion()
749 version = PullRequestVersion()
735 version.title = pull_request.title
750 version.title = pull_request.title
736 version.description = pull_request.description
751 version.description = pull_request.description
737 version.status = pull_request.status
752 version.status = pull_request.status
738 version.created_on = datetime.datetime.now()
753 version.created_on = datetime.datetime.now()
739 version.updated_on = pull_request.updated_on
754 version.updated_on = pull_request.updated_on
740 version.user_id = pull_request.user_id
755 version.user_id = pull_request.user_id
741 version.source_repo = pull_request.source_repo
756 version.source_repo = pull_request.source_repo
742 version.source_ref = pull_request.source_ref
757 version.source_ref = pull_request.source_ref
743 version.target_repo = pull_request.target_repo
758 version.target_repo = pull_request.target_repo
744 version.target_ref = pull_request.target_ref
759 version.target_ref = pull_request.target_ref
745
760
746 version._last_merge_source_rev = pull_request._last_merge_source_rev
761 version._last_merge_source_rev = pull_request._last_merge_source_rev
747 version._last_merge_target_rev = pull_request._last_merge_target_rev
762 version._last_merge_target_rev = pull_request._last_merge_target_rev
748 version._last_merge_status = pull_request._last_merge_status
763 version._last_merge_status = pull_request._last_merge_status
749 version.shadow_merge_ref = pull_request.shadow_merge_ref
764 version.shadow_merge_ref = pull_request.shadow_merge_ref
750 version.merge_rev = pull_request.merge_rev
765 version.merge_rev = pull_request.merge_rev
751
766
752 version.revisions = pull_request.revisions
767 version.revisions = pull_request.revisions
753 version.pull_request = pull_request
768 version.pull_request = pull_request
754 Session().add(version)
769 Session().add(version)
755 Session().flush()
770 Session().flush()
756
771
757 return version
772 return version
758
773
759 def _generate_update_diffs(self, pull_request, pull_request_version):
774 def _generate_update_diffs(self, pull_request, pull_request_version):
760
775
761 diff_context = (
776 diff_context = (
762 self.DIFF_CONTEXT +
777 self.DIFF_CONTEXT +
763 CommentsModel.needed_extra_diff_context())
778 CommentsModel.needed_extra_diff_context())
764
779
765 source_repo = pull_request_version.source_repo
780 source_repo = pull_request_version.source_repo
766 source_ref_id = pull_request_version.source_ref_parts.commit_id
781 source_ref_id = pull_request_version.source_ref_parts.commit_id
767 target_ref_id = pull_request_version.target_ref_parts.commit_id
782 target_ref_id = pull_request_version.target_ref_parts.commit_id
768 old_diff = self._get_diff_from_pr_or_version(
783 old_diff = self._get_diff_from_pr_or_version(
769 source_repo, source_ref_id, target_ref_id, context=diff_context)
784 source_repo, source_ref_id, target_ref_id, context=diff_context)
770
785
771 source_repo = pull_request.source_repo
786 source_repo = pull_request.source_repo
772 source_ref_id = pull_request.source_ref_parts.commit_id
787 source_ref_id = pull_request.source_ref_parts.commit_id
773 target_ref_id = pull_request.target_ref_parts.commit_id
788 target_ref_id = pull_request.target_ref_parts.commit_id
774
789
775 new_diff = self._get_diff_from_pr_or_version(
790 new_diff = self._get_diff_from_pr_or_version(
776 source_repo, source_ref_id, target_ref_id, context=diff_context)
791 source_repo, source_ref_id, target_ref_id, context=diff_context)
777
792
778 old_diff_data = diffs.DiffProcessor(old_diff)
793 old_diff_data = diffs.DiffProcessor(old_diff)
779 old_diff_data.prepare()
794 old_diff_data.prepare()
780 new_diff_data = diffs.DiffProcessor(new_diff)
795 new_diff_data = diffs.DiffProcessor(new_diff)
781 new_diff_data.prepare()
796 new_diff_data.prepare()
782
797
783 return old_diff_data, new_diff_data
798 return old_diff_data, new_diff_data
784
799
785 def _link_comments_to_version(self, pull_request_version):
800 def _link_comments_to_version(self, pull_request_version):
786 """
801 """
787 Link all unlinked comments of this pull request to the given version.
802 Link all unlinked comments of this pull request to the given version.
788
803
789 :param pull_request_version: The `PullRequestVersion` to which
804 :param pull_request_version: The `PullRequestVersion` to which
790 the comments shall be linked.
805 the comments shall be linked.
791
806
792 """
807 """
793 pull_request = pull_request_version.pull_request
808 pull_request = pull_request_version.pull_request
794 comments = ChangesetComment.query().filter(
809 comments = ChangesetComment.query().filter(
795 # TODO: johbo: Should we query for the repo at all here?
810 # TODO: johbo: Should we query for the repo at all here?
796 # Pending decision on how comments of PRs are to be related
811 # Pending decision on how comments of PRs are to be related
797 # to either the source repo, the target repo or no repo at all.
812 # to either the source repo, the target repo or no repo at all.
798 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
813 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
799 ChangesetComment.pull_request == pull_request,
814 ChangesetComment.pull_request == pull_request,
800 ChangesetComment.pull_request_version == None)
815 ChangesetComment.pull_request_version == None)
801
816
802 # TODO: johbo: Find out why this breaks if it is done in a bulk
817 # TODO: johbo: Find out why this breaks if it is done in a bulk
803 # operation.
818 # operation.
804 for comment in comments:
819 for comment in comments:
805 comment.pull_request_version_id = (
820 comment.pull_request_version_id = (
806 pull_request_version.pull_request_version_id)
821 pull_request_version.pull_request_version_id)
807 Session().add(comment)
822 Session().add(comment)
808
823
809 def _calculate_commit_id_changes(self, old_ids, new_ids):
824 def _calculate_commit_id_changes(self, old_ids, new_ids):
810 added = [x for x in new_ids if x not in old_ids]
825 added = [x for x in new_ids if x not in old_ids]
811 common = [x for x in new_ids if x in old_ids]
826 common = [x for x in new_ids if x in old_ids]
812 removed = [x for x in old_ids if x not in new_ids]
827 removed = [x for x in old_ids if x not in new_ids]
813 total = new_ids
828 total = new_ids
814 return ChangeTuple(added, common, removed, total)
829 return ChangeTuple(added, common, removed, total)
815
830
816 def _calculate_file_changes(self, old_diff_data, new_diff_data):
831 def _calculate_file_changes(self, old_diff_data, new_diff_data):
817
832
818 old_files = OrderedDict()
833 old_files = OrderedDict()
819 for diff_data in old_diff_data.parsed_diff:
834 for diff_data in old_diff_data.parsed_diff:
820 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
835 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
821
836
822 added_files = []
837 added_files = []
823 modified_files = []
838 modified_files = []
824 removed_files = []
839 removed_files = []
825 for diff_data in new_diff_data.parsed_diff:
840 for diff_data in new_diff_data.parsed_diff:
826 new_filename = diff_data['filename']
841 new_filename = diff_data['filename']
827 new_hash = md5_safe(diff_data['raw_diff'])
842 new_hash = md5_safe(diff_data['raw_diff'])
828
843
829 old_hash = old_files.get(new_filename)
844 old_hash = old_files.get(new_filename)
830 if not old_hash:
845 if not old_hash:
831 # file is not present in old diff, means it's added
846 # file is not present in old diff, means it's added
832 added_files.append(new_filename)
847 added_files.append(new_filename)
833 else:
848 else:
834 if new_hash != old_hash:
849 if new_hash != old_hash:
835 modified_files.append(new_filename)
850 modified_files.append(new_filename)
836 # now remove a file from old, since we have seen it already
851 # now remove a file from old, since we have seen it already
837 del old_files[new_filename]
852 del old_files[new_filename]
838
853
839 # removed files is when there are present in old, but not in NEW,
854 # removed files is when there are present in old, but not in NEW,
840 # since we remove old files that are present in new diff, left-overs
855 # since we remove old files that are present in new diff, left-overs
841 # if any should be the removed files
856 # if any should be the removed files
842 removed_files.extend(old_files.keys())
857 removed_files.extend(old_files.keys())
843
858
844 return FileChangeTuple(added_files, modified_files, removed_files)
859 return FileChangeTuple(added_files, modified_files, removed_files)
845
860
846 def _render_update_message(self, changes, file_changes):
861 def _render_update_message(self, changes, file_changes):
847 """
862 """
848 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
863 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
849 so it's always looking the same disregarding on which default
864 so it's always looking the same disregarding on which default
850 renderer system is using.
865 renderer system is using.
851
866
852 :param changes: changes named tuple
867 :param changes: changes named tuple
853 :param file_changes: file changes named tuple
868 :param file_changes: file changes named tuple
854
869
855 """
870 """
856 new_status = ChangesetStatus.get_status_lbl(
871 new_status = ChangesetStatus.get_status_lbl(
857 ChangesetStatus.STATUS_UNDER_REVIEW)
872 ChangesetStatus.STATUS_UNDER_REVIEW)
858
873
859 changed_files = (
874 changed_files = (
860 file_changes.added + file_changes.modified + file_changes.removed)
875 file_changes.added + file_changes.modified + file_changes.removed)
861
876
862 params = {
877 params = {
863 'under_review_label': new_status,
878 'under_review_label': new_status,
864 'added_commits': changes.added,
879 'added_commits': changes.added,
865 'removed_commits': changes.removed,
880 'removed_commits': changes.removed,
866 'changed_files': changed_files,
881 'changed_files': changed_files,
867 'added_files': file_changes.added,
882 'added_files': file_changes.added,
868 'modified_files': file_changes.modified,
883 'modified_files': file_changes.modified,
869 'removed_files': file_changes.removed,
884 'removed_files': file_changes.removed,
870 }
885 }
871 renderer = RstTemplateRenderer()
886 renderer = RstTemplateRenderer()
872 return renderer.render('pull_request_update.mako', **params)
887 return renderer.render('pull_request_update.mako', **params)
873
888
874 def edit(self, pull_request, title, description):
889 def edit(self, pull_request, title, description):
875 pull_request = self.__get_pull_request(pull_request)
890 pull_request = self.__get_pull_request(pull_request)
876 if pull_request.is_closed():
891 if pull_request.is_closed():
877 raise ValueError('This pull request is closed')
892 raise ValueError('This pull request is closed')
878 if title:
893 if title:
879 pull_request.title = title
894 pull_request.title = title
880 pull_request.description = description
895 pull_request.description = description
881 pull_request.updated_on = datetime.datetime.now()
896 pull_request.updated_on = datetime.datetime.now()
882 Session().add(pull_request)
897 Session().add(pull_request)
883
898
884 def update_reviewers(self, pull_request, reviewer_data):
899 def update_reviewers(self, pull_request, reviewer_data):
885 """
900 """
886 Update the reviewers in the pull request
901 Update the reviewers in the pull request
887
902
888 :param pull_request: the pr to update
903 :param pull_request: the pr to update
889 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
904 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
890 """
905 """
891
906
892 reviewers_reasons = {}
907 reviewers_reasons = {}
893 for user_id, reasons in reviewer_data:
908 for user_id, reasons in reviewer_data:
894 if isinstance(user_id, (int, basestring)):
909 if isinstance(user_id, (int, basestring)):
895 user_id = self._get_user(user_id).user_id
910 user_id = self._get_user(user_id).user_id
896 reviewers_reasons[user_id] = reasons
911 reviewers_reasons[user_id] = reasons
897
912
898 reviewers_ids = set(reviewers_reasons.keys())
913 reviewers_ids = set(reviewers_reasons.keys())
899 pull_request = self.__get_pull_request(pull_request)
914 pull_request = self.__get_pull_request(pull_request)
900 current_reviewers = PullRequestReviewers.query()\
915 current_reviewers = PullRequestReviewers.query()\
901 .filter(PullRequestReviewers.pull_request ==
916 .filter(PullRequestReviewers.pull_request ==
902 pull_request).all()
917 pull_request).all()
903 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
918 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
904
919
905 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
920 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
906 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
921 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
907
922
908 log.debug("Adding %s reviewers", ids_to_add)
923 log.debug("Adding %s reviewers", ids_to_add)
909 log.debug("Removing %s reviewers", ids_to_remove)
924 log.debug("Removing %s reviewers", ids_to_remove)
910 changed = False
925 changed = False
911 for uid in ids_to_add:
926 for uid in ids_to_add:
912 changed = True
927 changed = True
913 _usr = self._get_user(uid)
928 _usr = self._get_user(uid)
914 reasons = reviewers_reasons[uid]
929 reasons = reviewers_reasons[uid]
915 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
930 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
916 Session().add(reviewer)
931 Session().add(reviewer)
917
932
918 for uid in ids_to_remove:
933 for uid in ids_to_remove:
919 changed = True
934 changed = True
920 reviewers = PullRequestReviewers.query()\
935 reviewers = PullRequestReviewers.query()\
921 .filter(PullRequestReviewers.user_id == uid,
936 .filter(PullRequestReviewers.user_id == uid,
922 PullRequestReviewers.pull_request == pull_request)\
937 PullRequestReviewers.pull_request == pull_request)\
923 .all()
938 .all()
924 # use .all() in case we accidentally added the same person twice
939 # use .all() in case we accidentally added the same person twice
925 # this CAN happen due to the lack of DB checks
940 # this CAN happen due to the lack of DB checks
926 for obj in reviewers:
941 for obj in reviewers:
927 Session().delete(obj)
942 Session().delete(obj)
928
943
929 if changed:
944 if changed:
930 pull_request.updated_on = datetime.datetime.now()
945 pull_request.updated_on = datetime.datetime.now()
931 Session().add(pull_request)
946 Session().add(pull_request)
932
947
933 self.notify_reviewers(pull_request, ids_to_add)
948 self.notify_reviewers(pull_request, ids_to_add)
934 return ids_to_add, ids_to_remove
949 return ids_to_add, ids_to_remove
935
950
936 def get_url(self, pull_request):
951 def get_url(self, pull_request):
937 return h.url('pullrequest_show',
952 return h.url('pullrequest_show',
938 repo_name=safe_str(pull_request.target_repo.repo_name),
953 repo_name=safe_str(pull_request.target_repo.repo_name),
939 pull_request_id=pull_request.pull_request_id,
954 pull_request_id=pull_request.pull_request_id,
940 qualified=True)
955 qualified=True)
941
956
942 def get_shadow_clone_url(self, pull_request):
957 def get_shadow_clone_url(self, pull_request):
943 """
958 """
944 Returns qualified url pointing to the shadow repository. If this pull
959 Returns qualified url pointing to the shadow repository. If this pull
945 request is closed there is no shadow repository and ``None`` will be
960 request is closed there is no shadow repository and ``None`` will be
946 returned.
961 returned.
947 """
962 """
948 if pull_request.is_closed():
963 if pull_request.is_closed():
949 return None
964 return None
950 else:
965 else:
951 pr_url = urllib.unquote(self.get_url(pull_request))
966 pr_url = urllib.unquote(self.get_url(pull_request))
952 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
967 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
953
968
954 def notify_reviewers(self, pull_request, reviewers_ids):
969 def notify_reviewers(self, pull_request, reviewers_ids):
955 # notification to reviewers
970 # notification to reviewers
956 if not reviewers_ids:
971 if not reviewers_ids:
957 return
972 return
958
973
959 pull_request_obj = pull_request
974 pull_request_obj = pull_request
960 # get the current participants of this pull request
975 # get the current participants of this pull request
961 recipients = reviewers_ids
976 recipients = reviewers_ids
962 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
977 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
963
978
964 pr_source_repo = pull_request_obj.source_repo
979 pr_source_repo = pull_request_obj.source_repo
965 pr_target_repo = pull_request_obj.target_repo
980 pr_target_repo = pull_request_obj.target_repo
966
981
967 pr_url = h.url(
982 pr_url = h.url(
968 'pullrequest_show',
983 'pullrequest_show',
969 repo_name=pr_target_repo.repo_name,
984 repo_name=pr_target_repo.repo_name,
970 pull_request_id=pull_request_obj.pull_request_id,
985 pull_request_id=pull_request_obj.pull_request_id,
971 qualified=True,)
986 qualified=True,)
972
987
973 # set some variables for email notification
988 # set some variables for email notification
974 pr_target_repo_url = h.url(
989 pr_target_repo_url = h.url(
975 'summary_home',
990 'summary_home',
976 repo_name=pr_target_repo.repo_name,
991 repo_name=pr_target_repo.repo_name,
977 qualified=True)
992 qualified=True)
978
993
979 pr_source_repo_url = h.url(
994 pr_source_repo_url = h.url(
980 'summary_home',
995 'summary_home',
981 repo_name=pr_source_repo.repo_name,
996 repo_name=pr_source_repo.repo_name,
982 qualified=True)
997 qualified=True)
983
998
984 # pull request specifics
999 # pull request specifics
985 pull_request_commits = [
1000 pull_request_commits = [
986 (x.raw_id, x.message)
1001 (x.raw_id, x.message)
987 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1002 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
988
1003
989 kwargs = {
1004 kwargs = {
990 'user': pull_request.author,
1005 'user': pull_request.author,
991 'pull_request': pull_request_obj,
1006 'pull_request': pull_request_obj,
992 'pull_request_commits': pull_request_commits,
1007 'pull_request_commits': pull_request_commits,
993
1008
994 'pull_request_target_repo': pr_target_repo,
1009 'pull_request_target_repo': pr_target_repo,
995 'pull_request_target_repo_url': pr_target_repo_url,
1010 'pull_request_target_repo_url': pr_target_repo_url,
996
1011
997 'pull_request_source_repo': pr_source_repo,
1012 'pull_request_source_repo': pr_source_repo,
998 'pull_request_source_repo_url': pr_source_repo_url,
1013 'pull_request_source_repo_url': pr_source_repo_url,
999
1014
1000 'pull_request_url': pr_url,
1015 'pull_request_url': pr_url,
1001 }
1016 }
1002
1017
1003 # pre-generate the subject for notification itself
1018 # pre-generate the subject for notification itself
1004 (subject,
1019 (subject,
1005 _h, _e, # we don't care about those
1020 _h, _e, # we don't care about those
1006 body_plaintext) = EmailNotificationModel().render_email(
1021 body_plaintext) = EmailNotificationModel().render_email(
1007 notification_type, **kwargs)
1022 notification_type, **kwargs)
1008
1023
1009 # create notification objects, and emails
1024 # create notification objects, and emails
1010 NotificationModel().create(
1025 NotificationModel().create(
1011 created_by=pull_request.author,
1026 created_by=pull_request.author,
1012 notification_subject=subject,
1027 notification_subject=subject,
1013 notification_body=body_plaintext,
1028 notification_body=body_plaintext,
1014 notification_type=notification_type,
1029 notification_type=notification_type,
1015 recipients=recipients,
1030 recipients=recipients,
1016 email_kwargs=kwargs,
1031 email_kwargs=kwargs,
1017 )
1032 )
1018
1033
1019 def delete(self, pull_request):
1034 def delete(self, pull_request):
1020 pull_request = self.__get_pull_request(pull_request)
1035 pull_request = self.__get_pull_request(pull_request)
1021 self._cleanup_merge_workspace(pull_request)
1036 self._cleanup_merge_workspace(pull_request)
1022 Session().delete(pull_request)
1037 Session().delete(pull_request)
1023
1038
1024 def close_pull_request(self, pull_request, user):
1039 def close_pull_request(self, pull_request, user):
1025 pull_request = self.__get_pull_request(pull_request)
1040 pull_request = self.__get_pull_request(pull_request)
1026 self._cleanup_merge_workspace(pull_request)
1041 self._cleanup_merge_workspace(pull_request)
1027 pull_request.status = PullRequest.STATUS_CLOSED
1042 pull_request.status = PullRequest.STATUS_CLOSED
1028 pull_request.updated_on = datetime.datetime.now()
1043 pull_request.updated_on = datetime.datetime.now()
1029 Session().add(pull_request)
1044 Session().add(pull_request)
1030 self._trigger_pull_request_hook(
1045 self._trigger_pull_request_hook(
1031 pull_request, pull_request.author, 'close')
1046 pull_request, pull_request.author, 'close')
1032 self._log_action('user_closed_pull_request', user, pull_request)
1047 self._log_action('user_closed_pull_request', user, pull_request)
1033
1048
1034 def close_pull_request_with_comment(self, pull_request, user, repo,
1049 def close_pull_request_with_comment(self, pull_request, user, repo,
1035 message=None):
1050 message=None):
1036 status = ChangesetStatus.STATUS_REJECTED
1051 status = ChangesetStatus.STATUS_REJECTED
1037
1052
1038 if not message:
1053 if not message:
1039 message = (
1054 message = (
1040 _('Status change %(transition_icon)s %(status)s') % {
1055 _('Status change %(transition_icon)s %(status)s') % {
1041 'transition_icon': '>',
1056 'transition_icon': '>',
1042 'status': ChangesetStatus.get_status_lbl(status)})
1057 'status': ChangesetStatus.get_status_lbl(status)})
1043
1058
1044 internal_message = _('Closing with') + ' ' + message
1059 internal_message = _('Closing with') + ' ' + message
1045
1060
1046 comm = CommentsModel().create(
1061 comm = CommentsModel().create(
1047 text=internal_message,
1062 text=internal_message,
1048 repo=repo.repo_id,
1063 repo=repo.repo_id,
1049 user=user.user_id,
1064 user=user.user_id,
1050 pull_request=pull_request.pull_request_id,
1065 pull_request=pull_request.pull_request_id,
1051 f_path=None,
1066 f_path=None,
1052 line_no=None,
1067 line_no=None,
1053 status_change=ChangesetStatus.get_status_lbl(status),
1068 status_change=ChangesetStatus.get_status_lbl(status),
1054 status_change_type=status,
1069 status_change_type=status,
1055 closing_pr=True
1070 closing_pr=True
1056 )
1071 )
1057
1072
1058 ChangesetStatusModel().set_status(
1073 ChangesetStatusModel().set_status(
1059 repo.repo_id,
1074 repo.repo_id,
1060 status,
1075 status,
1061 user.user_id,
1076 user.user_id,
1062 comm,
1077 comm,
1063 pull_request=pull_request.pull_request_id
1078 pull_request=pull_request.pull_request_id
1064 )
1079 )
1065 Session().flush()
1080 Session().flush()
1066
1081
1067 PullRequestModel().close_pull_request(
1082 PullRequestModel().close_pull_request(
1068 pull_request.pull_request_id, user)
1083 pull_request.pull_request_id, user)
1069
1084
1070 def merge_status(self, pull_request):
1085 def merge_status(self, pull_request):
1071 if not self._is_merge_enabled(pull_request):
1086 if not self._is_merge_enabled(pull_request):
1072 return False, _('Server-side pull request merging is disabled.')
1087 return False, _('Server-side pull request merging is disabled.')
1073 if pull_request.is_closed():
1088 if pull_request.is_closed():
1074 return False, _('This pull request is closed.')
1089 return False, _('This pull request is closed.')
1075 merge_possible, msg = self._check_repo_requirements(
1090 merge_possible, msg = self._check_repo_requirements(
1076 target=pull_request.target_repo, source=pull_request.source_repo)
1091 target=pull_request.target_repo, source=pull_request.source_repo)
1077 if not merge_possible:
1092 if not merge_possible:
1078 return merge_possible, msg
1093 return merge_possible, msg
1079
1094
1080 try:
1095 try:
1081 resp = self._try_merge(pull_request)
1096 resp = self._try_merge(pull_request)
1082 log.debug("Merge response: %s", resp)
1097 log.debug("Merge response: %s", resp)
1083 status = resp.possible, self.merge_status_message(
1098 status = resp.possible, self.merge_status_message(
1084 resp.failure_reason)
1099 resp.failure_reason)
1085 except NotImplementedError:
1100 except NotImplementedError:
1086 status = False, _('Pull request merging is not supported.')
1101 status = False, _('Pull request merging is not supported.')
1087
1102
1088 return status
1103 return status
1089
1104
1090 def _check_repo_requirements(self, target, source):
1105 def _check_repo_requirements(self, target, source):
1091 """
1106 """
1092 Check if `target` and `source` have compatible requirements.
1107 Check if `target` and `source` have compatible requirements.
1093
1108
1094 Currently this is just checking for largefiles.
1109 Currently this is just checking for largefiles.
1095 """
1110 """
1096 target_has_largefiles = self._has_largefiles(target)
1111 target_has_largefiles = self._has_largefiles(target)
1097 source_has_largefiles = self._has_largefiles(source)
1112 source_has_largefiles = self._has_largefiles(source)
1098 merge_possible = True
1113 merge_possible = True
1099 message = u''
1114 message = u''
1100
1115
1101 if target_has_largefiles != source_has_largefiles:
1116 if target_has_largefiles != source_has_largefiles:
1102 merge_possible = False
1117 merge_possible = False
1103 if source_has_largefiles:
1118 if source_has_largefiles:
1104 message = _(
1119 message = _(
1105 'Target repository large files support is disabled.')
1120 'Target repository large files support is disabled.')
1106 else:
1121 else:
1107 message = _(
1122 message = _(
1108 'Source repository large files support is disabled.')
1123 'Source repository large files support is disabled.')
1109
1124
1110 return merge_possible, message
1125 return merge_possible, message
1111
1126
1112 def _has_largefiles(self, repo):
1127 def _has_largefiles(self, repo):
1113 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1128 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1114 'extensions', 'largefiles')
1129 'extensions', 'largefiles')
1115 return largefiles_ui and largefiles_ui[0].active
1130 return largefiles_ui and largefiles_ui[0].active
1116
1131
1117 def _try_merge(self, pull_request):
1132 def _try_merge(self, pull_request):
1118 """
1133 """
1119 Try to merge the pull request and return the merge status.
1134 Try to merge the pull request and return the merge status.
1120 """
1135 """
1121 log.debug(
1136 log.debug(
1122 "Trying out if the pull request %s can be merged.",
1137 "Trying out if the pull request %s can be merged.",
1123 pull_request.pull_request_id)
1138 pull_request.pull_request_id)
1124 target_vcs = pull_request.target_repo.scm_instance()
1139 target_vcs = pull_request.target_repo.scm_instance()
1125
1140
1126 # Refresh the target reference.
1141 # Refresh the target reference.
1127 try:
1142 try:
1128 target_ref = self._refresh_reference(
1143 target_ref = self._refresh_reference(
1129 pull_request.target_ref_parts, target_vcs)
1144 pull_request.target_ref_parts, target_vcs)
1130 except CommitDoesNotExistError:
1145 except CommitDoesNotExistError:
1131 merge_state = MergeResponse(
1146 merge_state = MergeResponse(
1132 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1147 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1133 return merge_state
1148 return merge_state
1134
1149
1135 target_locked = pull_request.target_repo.locked
1150 target_locked = pull_request.target_repo.locked
1136 if target_locked and target_locked[0]:
1151 if target_locked and target_locked[0]:
1137 log.debug("The target repository is locked.")
1152 log.debug("The target repository is locked.")
1138 merge_state = MergeResponse(
1153 merge_state = MergeResponse(
1139 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1154 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1140 elif self._needs_merge_state_refresh(pull_request, target_ref):
1155 elif self._needs_merge_state_refresh(pull_request, target_ref):
1141 log.debug("Refreshing the merge status of the repository.")
1156 log.debug("Refreshing the merge status of the repository.")
1142 merge_state = self._refresh_merge_state(
1157 merge_state = self._refresh_merge_state(
1143 pull_request, target_vcs, target_ref)
1158 pull_request, target_vcs, target_ref)
1144 else:
1159 else:
1145 possible = pull_request.\
1160 possible = pull_request.\
1146 _last_merge_status == MergeFailureReason.NONE
1161 _last_merge_status == MergeFailureReason.NONE
1147 merge_state = MergeResponse(
1162 merge_state = MergeResponse(
1148 possible, False, None, pull_request._last_merge_status)
1163 possible, False, None, pull_request._last_merge_status)
1149
1164
1150 return merge_state
1165 return merge_state
1151
1166
1152 def _refresh_reference(self, reference, vcs_repository):
1167 def _refresh_reference(self, reference, vcs_repository):
1153 if reference.type in ('branch', 'book'):
1168 if reference.type in ('branch', 'book'):
1154 name_or_id = reference.name
1169 name_or_id = reference.name
1155 else:
1170 else:
1156 name_or_id = reference.commit_id
1171 name_or_id = reference.commit_id
1157 refreshed_commit = vcs_repository.get_commit(name_or_id)
1172 refreshed_commit = vcs_repository.get_commit(name_or_id)
1158 refreshed_reference = Reference(
1173 refreshed_reference = Reference(
1159 reference.type, reference.name, refreshed_commit.raw_id)
1174 reference.type, reference.name, refreshed_commit.raw_id)
1160 return refreshed_reference
1175 return refreshed_reference
1161
1176
1162 def _needs_merge_state_refresh(self, pull_request, target_reference):
1177 def _needs_merge_state_refresh(self, pull_request, target_reference):
1163 return not(
1178 return not(
1164 pull_request.revisions and
1179 pull_request.revisions and
1165 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1180 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1166 target_reference.commit_id == pull_request._last_merge_target_rev)
1181 target_reference.commit_id == pull_request._last_merge_target_rev)
1167
1182
1168 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1183 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1169 workspace_id = self._workspace_id(pull_request)
1184 workspace_id = self._workspace_id(pull_request)
1170 source_vcs = pull_request.source_repo.scm_instance()
1185 source_vcs = pull_request.source_repo.scm_instance()
1171 use_rebase = self._use_rebase_for_merging(pull_request)
1186 use_rebase = self._use_rebase_for_merging(pull_request)
1172 merge_state = target_vcs.merge(
1187 merge_state = target_vcs.merge(
1173 target_reference, source_vcs, pull_request.source_ref_parts,
1188 target_reference, source_vcs, pull_request.source_ref_parts,
1174 workspace_id, dry_run=True, use_rebase=use_rebase)
1189 workspace_id, dry_run=True, use_rebase=use_rebase)
1175
1190
1176 # Do not store the response if there was an unknown error.
1191 # Do not store the response if there was an unknown error.
1177 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1192 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1178 pull_request._last_merge_source_rev = \
1193 pull_request._last_merge_source_rev = \
1179 pull_request.source_ref_parts.commit_id
1194 pull_request.source_ref_parts.commit_id
1180 pull_request._last_merge_target_rev = target_reference.commit_id
1195 pull_request._last_merge_target_rev = target_reference.commit_id
1181 pull_request._last_merge_status = merge_state.failure_reason
1196 pull_request._last_merge_status = merge_state.failure_reason
1182 pull_request.shadow_merge_ref = merge_state.merge_ref
1197 pull_request.shadow_merge_ref = merge_state.merge_ref
1183 Session().add(pull_request)
1198 Session().add(pull_request)
1184 Session().commit()
1199 Session().commit()
1185
1200
1186 return merge_state
1201 return merge_state
1187
1202
1188 def _workspace_id(self, pull_request):
1203 def _workspace_id(self, pull_request):
1189 workspace_id = 'pr-%s' % pull_request.pull_request_id
1204 workspace_id = 'pr-%s' % pull_request.pull_request_id
1190 return workspace_id
1205 return workspace_id
1191
1206
1192 def merge_status_message(self, status_code):
1207 def merge_status_message(self, status_code):
1193 """
1208 """
1194 Return a human friendly error message for the given merge status code.
1209 Return a human friendly error message for the given merge status code.
1195 """
1210 """
1196 return self.MERGE_STATUS_MESSAGES[status_code]
1211 return self.MERGE_STATUS_MESSAGES[status_code]
1197
1212
1198 def generate_repo_data(self, repo, commit_id=None, branch=None,
1213 def generate_repo_data(self, repo, commit_id=None, branch=None,
1199 bookmark=None):
1214 bookmark=None):
1200 all_refs, selected_ref = \
1215 all_refs, selected_ref = \
1201 self._get_repo_pullrequest_sources(
1216 self._get_repo_pullrequest_sources(
1202 repo.scm_instance(), commit_id=commit_id,
1217 repo.scm_instance(), commit_id=commit_id,
1203 branch=branch, bookmark=bookmark)
1218 branch=branch, bookmark=bookmark)
1204
1219
1205 refs_select2 = []
1220 refs_select2 = []
1206 for element in all_refs:
1221 for element in all_refs:
1207 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1222 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1208 refs_select2.append({'text': element[1], 'children': children})
1223 refs_select2.append({'text': element[1], 'children': children})
1209
1224
1210 return {
1225 return {
1211 'user': {
1226 'user': {
1212 'user_id': repo.user.user_id,
1227 'user_id': repo.user.user_id,
1213 'username': repo.user.username,
1228 'username': repo.user.username,
1214 'firstname': repo.user.firstname,
1229 'firstname': repo.user.firstname,
1215 'lastname': repo.user.lastname,
1230 'lastname': repo.user.lastname,
1216 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1231 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1217 },
1232 },
1218 'description': h.chop_at_smart(repo.description, '\n'),
1233 'description': h.chop_at_smart(repo.description, '\n'),
1219 'refs': {
1234 'refs': {
1220 'all_refs': all_refs,
1235 'all_refs': all_refs,
1221 'selected_ref': selected_ref,
1236 'selected_ref': selected_ref,
1222 'select2_refs': refs_select2
1237 'select2_refs': refs_select2
1223 }
1238 }
1224 }
1239 }
1225
1240
1226 def generate_pullrequest_title(self, source, source_ref, target):
1241 def generate_pullrequest_title(self, source, source_ref, target):
1227 return u'{source}#{at_ref} to {target}'.format(
1242 return u'{source}#{at_ref} to {target}'.format(
1228 source=source,
1243 source=source,
1229 at_ref=source_ref,
1244 at_ref=source_ref,
1230 target=target,
1245 target=target,
1231 )
1246 )
1232
1247
1233 def _cleanup_merge_workspace(self, pull_request):
1248 def _cleanup_merge_workspace(self, pull_request):
1234 # Merging related cleanup
1249 # Merging related cleanup
1235 target_scm = pull_request.target_repo.scm_instance()
1250 target_scm = pull_request.target_repo.scm_instance()
1236 workspace_id = 'pr-%s' % pull_request.pull_request_id
1251 workspace_id = 'pr-%s' % pull_request.pull_request_id
1237
1252
1238 try:
1253 try:
1239 target_scm.cleanup_merge_workspace(workspace_id)
1254 target_scm.cleanup_merge_workspace(workspace_id)
1240 except NotImplementedError:
1255 except NotImplementedError:
1241 pass
1256 pass
1242
1257
1243 def _get_repo_pullrequest_sources(
1258 def _get_repo_pullrequest_sources(
1244 self, repo, commit_id=None, branch=None, bookmark=None):
1259 self, repo, commit_id=None, branch=None, bookmark=None):
1245 """
1260 """
1246 Return a structure with repo's interesting commits, suitable for
1261 Return a structure with repo's interesting commits, suitable for
1247 the selectors in pullrequest controller
1262 the selectors in pullrequest controller
1248
1263
1249 :param commit_id: a commit that must be in the list somehow
1264 :param commit_id: a commit that must be in the list somehow
1250 and selected by default
1265 and selected by default
1251 :param branch: a branch that must be in the list and selected
1266 :param branch: a branch that must be in the list and selected
1252 by default - even if closed
1267 by default - even if closed
1253 :param bookmark: a bookmark that must be in the list and selected
1268 :param bookmark: a bookmark that must be in the list and selected
1254 """
1269 """
1255
1270
1256 commit_id = safe_str(commit_id) if commit_id else None
1271 commit_id = safe_str(commit_id) if commit_id else None
1257 branch = safe_str(branch) if branch else None
1272 branch = safe_str(branch) if branch else None
1258 bookmark = safe_str(bookmark) if bookmark else None
1273 bookmark = safe_str(bookmark) if bookmark else None
1259
1274
1260 selected = None
1275 selected = None
1261
1276
1262 # order matters: first source that has commit_id in it will be selected
1277 # order matters: first source that has commit_id in it will be selected
1263 sources = []
1278 sources = []
1264 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1279 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1265 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1280 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1266
1281
1267 if commit_id:
1282 if commit_id:
1268 ref_commit = (h.short_id(commit_id), commit_id)
1283 ref_commit = (h.short_id(commit_id), commit_id)
1269 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1284 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1270
1285
1271 sources.append(
1286 sources.append(
1272 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1287 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1273 )
1288 )
1274
1289
1275 groups = []
1290 groups = []
1276 for group_key, ref_list, group_name, match in sources:
1291 for group_key, ref_list, group_name, match in sources:
1277 group_refs = []
1292 group_refs = []
1278 for ref_name, ref_id in ref_list:
1293 for ref_name, ref_id in ref_list:
1279 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1294 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1280 group_refs.append((ref_key, ref_name))
1295 group_refs.append((ref_key, ref_name))
1281
1296
1282 if not selected:
1297 if not selected:
1283 if set([commit_id, match]) & set([ref_id, ref_name]):
1298 if set([commit_id, match]) & set([ref_id, ref_name]):
1284 selected = ref_key
1299 selected = ref_key
1285
1300
1286 if group_refs:
1301 if group_refs:
1287 groups.append((group_refs, group_name))
1302 groups.append((group_refs, group_name))
1288
1303
1289 if not selected:
1304 if not selected:
1290 ref = commit_id or branch or bookmark
1305 ref = commit_id or branch or bookmark
1291 if ref:
1306 if ref:
1292 raise CommitDoesNotExistError(
1307 raise CommitDoesNotExistError(
1293 'No commit refs could be found matching: %s' % ref)
1308 'No commit refs could be found matching: %s' % ref)
1294 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1309 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1295 selected = 'branch:%s:%s' % (
1310 selected = 'branch:%s:%s' % (
1296 repo.DEFAULT_BRANCH_NAME,
1311 repo.DEFAULT_BRANCH_NAME,
1297 repo.branches[repo.DEFAULT_BRANCH_NAME]
1312 repo.branches[repo.DEFAULT_BRANCH_NAME]
1298 )
1313 )
1299 elif repo.commit_ids:
1314 elif repo.commit_ids:
1300 rev = repo.commit_ids[0]
1315 rev = repo.commit_ids[0]
1301 selected = 'rev:%s:%s' % (rev, rev)
1316 selected = 'rev:%s:%s' % (rev, rev)
1302 else:
1317 else:
1303 raise EmptyRepositoryError()
1318 raise EmptyRepositoryError()
1304 return groups, selected
1319 return groups, selected
1305
1320
1306 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1321 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1307 return self._get_diff_from_pr_or_version(
1322 return self._get_diff_from_pr_or_version(
1308 source_repo, source_ref_id, target_ref_id, context=context)
1323 source_repo, source_ref_id, target_ref_id, context=context)
1309
1324
1310 def _get_diff_from_pr_or_version(
1325 def _get_diff_from_pr_or_version(
1311 self, source_repo, source_ref_id, target_ref_id, context):
1326 self, source_repo, source_ref_id, target_ref_id, context):
1312 target_commit = source_repo.get_commit(
1327 target_commit = source_repo.get_commit(
1313 commit_id=safe_str(target_ref_id))
1328 commit_id=safe_str(target_ref_id))
1314 source_commit = source_repo.get_commit(
1329 source_commit = source_repo.get_commit(
1315 commit_id=safe_str(source_ref_id))
1330 commit_id=safe_str(source_ref_id))
1316 if isinstance(source_repo, Repository):
1331 if isinstance(source_repo, Repository):
1317 vcs_repo = source_repo.scm_instance()
1332 vcs_repo = source_repo.scm_instance()
1318 else:
1333 else:
1319 vcs_repo = source_repo
1334 vcs_repo = source_repo
1320
1335
1321 # TODO: johbo: In the context of an update, we cannot reach
1336 # TODO: johbo: In the context of an update, we cannot reach
1322 # the old commit anymore with our normal mechanisms. It needs
1337 # the old commit anymore with our normal mechanisms. It needs
1323 # some sort of special support in the vcs layer to avoid this
1338 # some sort of special support in the vcs layer to avoid this
1324 # workaround.
1339 # workaround.
1325 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1340 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1326 vcs_repo.alias == 'git'):
1341 vcs_repo.alias == 'git'):
1327 source_commit.raw_id = safe_str(source_ref_id)
1342 source_commit.raw_id = safe_str(source_ref_id)
1328
1343
1329 log.debug('calculating diff between '
1344 log.debug('calculating diff between '
1330 'source_ref:%s and target_ref:%s for repo `%s`',
1345 'source_ref:%s and target_ref:%s for repo `%s`',
1331 target_ref_id, source_ref_id,
1346 target_ref_id, source_ref_id,
1332 safe_unicode(vcs_repo.path))
1347 safe_unicode(vcs_repo.path))
1333
1348
1334 vcs_diff = vcs_repo.get_diff(
1349 vcs_diff = vcs_repo.get_diff(
1335 commit1=target_commit, commit2=source_commit, context=context)
1350 commit1=target_commit, commit2=source_commit, context=context)
1336 return vcs_diff
1351 return vcs_diff
1337
1352
1338 def _is_merge_enabled(self, pull_request):
1353 def _is_merge_enabled(self, pull_request):
1339 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1354 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1340 settings = settings_model.get_general_settings()
1355 settings = settings_model.get_general_settings()
1341 return settings.get('rhodecode_pr_merge_enabled', False)
1356 return settings.get('rhodecode_pr_merge_enabled', False)
1342
1357
1343 def _use_rebase_for_merging(self, pull_request):
1358 def _use_rebase_for_merging(self, pull_request):
1344 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1359 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1345 settings = settings_model.get_general_settings()
1360 settings = settings_model.get_general_settings()
1346 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1361 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1347
1362
1348 def _log_action(self, action, user, pull_request):
1363 def _log_action(self, action, user, pull_request):
1349 action_logger(
1364 action_logger(
1350 user,
1365 user,
1351 '{action}:{pr_id}'.format(
1366 '{action}:{pr_id}'.format(
1352 action=action, pr_id=pull_request.pull_request_id),
1367 action=action, pr_id=pull_request.pull_request_id),
1353 pull_request.target_repo)
1368 pull_request.target_repo)
1354
1369
1355
1370
1356 class MergeCheck(object):
1371 class MergeCheck(object):
1357 """
1372 """
1358 Perform Merge Checks and returns a check object which stores information
1373 Perform Merge Checks and returns a check object which stores information
1359 about merge errors, and merge conditions
1374 about merge errors, and merge conditions
1360 """
1375 """
1361 TODO_CHECK = 'todo'
1376 TODO_CHECK = 'todo'
1362 PERM_CHECK = 'perm'
1377 PERM_CHECK = 'perm'
1363 REVIEW_CHECK = 'review'
1378 REVIEW_CHECK = 'review'
1364 MERGE_CHECK = 'merge'
1379 MERGE_CHECK = 'merge'
1365
1380
1366 def __init__(self):
1381 def __init__(self):
1367 self.review_status = None
1382 self.review_status = None
1368 self.merge_possible = None
1383 self.merge_possible = None
1369 self.merge_msg = ''
1384 self.merge_msg = ''
1370 self.failed = None
1385 self.failed = None
1371 self.errors = []
1386 self.errors = []
1372 self.error_details = OrderedDict()
1387 self.error_details = OrderedDict()
1373
1388
1374 def push_error(self, error_type, message, error_key, details):
1389 def push_error(self, error_type, message, error_key, details):
1375 self.failed = True
1390 self.failed = True
1376 self.errors.append([error_type, message])
1391 self.errors.append([error_type, message])
1377 self.error_details[error_key] = dict(
1392 self.error_details[error_key] = dict(
1378 details=details,
1393 details=details,
1379 error_type=error_type,
1394 error_type=error_type,
1380 message=message
1395 message=message
1381 )
1396 )
1382
1397
1383 @classmethod
1398 @classmethod
1384 def validate(cls, pull_request, user, fail_early=False, translator=None):
1399 def validate(cls, pull_request, user, fail_early=False, translator=None):
1385 # if migrated to pyramid...
1400 # if migrated to pyramid...
1386 # _ = lambda: translator or _ # use passed in translator if any
1401 # _ = lambda: translator or _ # use passed in translator if any
1387
1402
1388 merge_check = cls()
1403 merge_check = cls()
1389
1404
1390 # permissions to merge
1405 # permissions to merge
1391 user_allowed_to_merge = PullRequestModel().check_user_merge(
1406 user_allowed_to_merge = PullRequestModel().check_user_merge(
1392 pull_request, user)
1407 pull_request, user)
1393 if not user_allowed_to_merge:
1408 if not user_allowed_to_merge:
1394 log.debug("MergeCheck: cannot merge, approval is pending.")
1409 log.debug("MergeCheck: cannot merge, approval is pending.")
1395
1410
1396 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1411 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1397 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1412 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1398 if fail_early:
1413 if fail_early:
1399 return merge_check
1414 return merge_check
1400
1415
1401 # review status, must be always present
1416 # review status, must be always present
1402 review_status = pull_request.calculated_review_status()
1417 review_status = pull_request.calculated_review_status()
1403 merge_check.review_status = review_status
1418 merge_check.review_status = review_status
1404
1419
1405 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1420 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1406 if not status_approved:
1421 if not status_approved:
1407 log.debug("MergeCheck: cannot merge, approval is pending.")
1422 log.debug("MergeCheck: cannot merge, approval is pending.")
1408
1423
1409 msg = _('Pull request reviewer approval is pending.')
1424 msg = _('Pull request reviewer approval is pending.')
1410
1425
1411 merge_check.push_error(
1426 merge_check.push_error(
1412 'warning', msg, cls.REVIEW_CHECK, review_status)
1427 'warning', msg, cls.REVIEW_CHECK, review_status)
1413
1428
1414 if fail_early:
1429 if fail_early:
1415 return merge_check
1430 return merge_check
1416
1431
1417 # left over TODOs
1432 # left over TODOs
1418 todos = CommentsModel().get_unresolved_todos(pull_request)
1433 todos = CommentsModel().get_unresolved_todos(pull_request)
1419 if todos:
1434 if todos:
1420 log.debug("MergeCheck: cannot merge, {} "
1435 log.debug("MergeCheck: cannot merge, {} "
1421 "unresolved todos left.".format(len(todos)))
1436 "unresolved todos left.".format(len(todos)))
1422
1437
1423 if len(todos) == 1:
1438 if len(todos) == 1:
1424 msg = _('Cannot merge, {} TODO still not resolved.').format(
1439 msg = _('Cannot merge, {} TODO still not resolved.').format(
1425 len(todos))
1440 len(todos))
1426 else:
1441 else:
1427 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1442 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1428 len(todos))
1443 len(todos))
1429
1444
1430 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1445 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1431
1446
1432 if fail_early:
1447 if fail_early:
1433 return merge_check
1448 return merge_check
1434
1449
1435 # merge possible
1450 # merge possible
1436 merge_status, msg = PullRequestModel().merge_status(pull_request)
1451 merge_status, msg = PullRequestModel().merge_status(pull_request)
1437 merge_check.merge_possible = merge_status
1452 merge_check.merge_possible = merge_status
1438 merge_check.merge_msg = msg
1453 merge_check.merge_msg = msg
1439 if not merge_status:
1454 if not merge_status:
1440 log.debug(
1455 log.debug(
1441 "MergeCheck: cannot merge, pull request merge not possible.")
1456 "MergeCheck: cannot merge, pull request merge not possible.")
1442 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1457 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1443
1458
1444 if fail_early:
1459 if fail_early:
1445 return merge_check
1460 return merge_check
1446
1461
1447 return merge_check
1462 return merge_check
1448
1463
1449
1464
1450 ChangeTuple = namedtuple('ChangeTuple',
1465 ChangeTuple = namedtuple('ChangeTuple',
1451 ['added', 'common', 'removed', 'total'])
1466 ['added', 'common', 'removed', 'total'])
1452
1467
1453 FileChangeTuple = namedtuple('FileChangeTuple',
1468 FileChangeTuple = namedtuple('FileChangeTuple',
1454 ['added', 'modified', 'removed'])
1469 ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now