##// END OF EJS Templates
pr: Refactor PR update error message handling.
Martin Bornhold -
r1082:b76addfd default
parent child Browse files
Show More
@@ -1,890 +1,887 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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
24
25 import peppercorn
25 import peppercorn
26 import formencode
26 import formencode
27 import logging
27 import logging
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 from pylons import request, tmpl_context as c, url
30 from pylons import request, tmpl_context as c, url
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from sqlalchemy.sql import func
34 from sqlalchemy.sql import func
35 from sqlalchemy.sql.expression import or_
35 from sqlalchemy.sql.expression import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import auth, diffs, helpers as h
38 from rhodecode.lib import auth, diffs, helpers as h
39 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.base import (
40 from rhodecode.lib.base import (
41 BaseRepoController, render, vcs_operation_context)
41 BaseRepoController, render, vcs_operation_context)
42 from rhodecode.lib.auth import (
42 from rhodecode.lib.auth import (
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
44 HasAcceptedRepoType, XHRRequired)
44 HasAcceptedRepoType, XHRRequired)
45 from rhodecode.lib.channelstream import channelstream_request
45 from rhodecode.lib.channelstream import channelstream_request
46 from rhodecode.lib.utils import jsonify
46 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
47 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
48 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
48 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
49 from rhodecode.lib.vcs.exceptions import (
49 from rhodecode.lib.vcs.exceptions import (
50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
51 from rhodecode.lib.diffs import LimitedDiffContainer
51 from rhodecode.lib.diffs import LimitedDiffContainer
52 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.changeset_status import ChangesetStatusModel
53 from rhodecode.model.comment import ChangesetCommentsModel
53 from rhodecode.model.comment import ChangesetCommentsModel
54 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
54 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
55 Repository
55 Repository
56 from rhodecode.model.forms import PullRequestForm
56 from rhodecode.model.forms import PullRequestForm
57 from rhodecode.model.meta import Session
57 from rhodecode.model.meta import Session
58 from rhodecode.model.pull_request import PullRequestModel
58 from rhodecode.model.pull_request import PullRequestModel
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 class PullrequestsController(BaseRepoController):
63 class PullrequestsController(BaseRepoController):
64 def __before__(self):
64 def __before__(self):
65 super(PullrequestsController, self).__before__()
65 super(PullrequestsController, self).__before__()
66
66
67 def _load_compare_data(self, pull_request, enable_comments=True):
67 def _load_compare_data(self, pull_request, enable_comments=True):
68 """
68 """
69 Load context data needed for generating compare diff
69 Load context data needed for generating compare diff
70
70
71 :param pull_request: object related to the request
71 :param pull_request: object related to the request
72 :param enable_comments: flag to determine if comments are included
72 :param enable_comments: flag to determine if comments are included
73 """
73 """
74 source_repo = pull_request.source_repo
74 source_repo = pull_request.source_repo
75 source_ref_id = pull_request.source_ref_parts.commit_id
75 source_ref_id = pull_request.source_ref_parts.commit_id
76
76
77 target_repo = pull_request.target_repo
77 target_repo = pull_request.target_repo
78 target_ref_id = pull_request.target_ref_parts.commit_id
78 target_ref_id = pull_request.target_ref_parts.commit_id
79
79
80 # despite opening commits for bookmarks/branches/tags, we always
80 # despite opening commits for bookmarks/branches/tags, we always
81 # convert this to rev to prevent changes after bookmark or branch change
81 # convert this to rev to prevent changes after bookmark or branch change
82 c.source_ref_type = 'rev'
82 c.source_ref_type = 'rev'
83 c.source_ref = source_ref_id
83 c.source_ref = source_ref_id
84
84
85 c.target_ref_type = 'rev'
85 c.target_ref_type = 'rev'
86 c.target_ref = target_ref_id
86 c.target_ref = target_ref_id
87
87
88 c.source_repo = source_repo
88 c.source_repo = source_repo
89 c.target_repo = target_repo
89 c.target_repo = target_repo
90
90
91 c.fulldiff = bool(request.GET.get('fulldiff'))
91 c.fulldiff = bool(request.GET.get('fulldiff'))
92
92
93 # diff_limit is the old behavior, will cut off the whole diff
93 # diff_limit is the old behavior, will cut off the whole diff
94 # if the limit is applied otherwise will just hide the
94 # if the limit is applied otherwise will just hide the
95 # big files from the front-end
95 # big files from the front-end
96 diff_limit = self.cut_off_limit_diff
96 diff_limit = self.cut_off_limit_diff
97 file_limit = self.cut_off_limit_file
97 file_limit = self.cut_off_limit_file
98
98
99 pre_load = ["author", "branch", "date", "message"]
99 pre_load = ["author", "branch", "date", "message"]
100
100
101 c.commit_ranges = []
101 c.commit_ranges = []
102 source_commit = EmptyCommit()
102 source_commit = EmptyCommit()
103 target_commit = EmptyCommit()
103 target_commit = EmptyCommit()
104 c.missing_requirements = False
104 c.missing_requirements = False
105 try:
105 try:
106 c.commit_ranges = [
106 c.commit_ranges = [
107 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
107 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
108 for rev in pull_request.revisions]
108 for rev in pull_request.revisions]
109
109
110 c.statuses = source_repo.statuses(
110 c.statuses = source_repo.statuses(
111 [x.raw_id for x in c.commit_ranges])
111 [x.raw_id for x in c.commit_ranges])
112
112
113 target_commit = source_repo.get_commit(
113 target_commit = source_repo.get_commit(
114 commit_id=safe_str(target_ref_id))
114 commit_id=safe_str(target_ref_id))
115 source_commit = source_repo.get_commit(
115 source_commit = source_repo.get_commit(
116 commit_id=safe_str(source_ref_id))
116 commit_id=safe_str(source_ref_id))
117 except RepositoryRequirementError:
117 except RepositoryRequirementError:
118 c.missing_requirements = True
118 c.missing_requirements = True
119
119
120 c.missing_commits = False
120 c.missing_commits = False
121 if (c.missing_requirements or
121 if (c.missing_requirements or
122 isinstance(source_commit, EmptyCommit) or
122 isinstance(source_commit, EmptyCommit) or
123 source_commit == target_commit):
123 source_commit == target_commit):
124 _parsed = []
124 _parsed = []
125 c.missing_commits = True
125 c.missing_commits = True
126 else:
126 else:
127 vcs_diff = PullRequestModel().get_diff(pull_request)
127 vcs_diff = PullRequestModel().get_diff(pull_request)
128 diff_processor = diffs.DiffProcessor(
128 diff_processor = diffs.DiffProcessor(
129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
130 file_limit=file_limit, show_full_diff=c.fulldiff)
130 file_limit=file_limit, show_full_diff=c.fulldiff)
131 _parsed = diff_processor.prepare()
131 _parsed = diff_processor.prepare()
132
132
133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
134
134
135 c.files = []
135 c.files = []
136 c.changes = {}
136 c.changes = {}
137 c.lines_added = 0
137 c.lines_added = 0
138 c.lines_deleted = 0
138 c.lines_deleted = 0
139 c.included_files = []
139 c.included_files = []
140 c.deleted_files = []
140 c.deleted_files = []
141
141
142 for f in _parsed:
142 for f in _parsed:
143 st = f['stats']
143 st = f['stats']
144 c.lines_added += st['added']
144 c.lines_added += st['added']
145 c.lines_deleted += st['deleted']
145 c.lines_deleted += st['deleted']
146
146
147 fid = h.FID('', f['filename'])
147 fid = h.FID('', f['filename'])
148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
149 c.included_files.append(f['filename'])
149 c.included_files.append(f['filename'])
150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
151 parsed_lines=[f])
151 parsed_lines=[f])
152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
153
153
154 def _extract_ordering(self, request):
154 def _extract_ordering(self, request):
155 column_index = safe_int(request.GET.get('order[0][column]'))
155 column_index = safe_int(request.GET.get('order[0][column]'))
156 order_dir = request.GET.get('order[0][dir]', 'desc')
156 order_dir = request.GET.get('order[0][dir]', 'desc')
157 order_by = request.GET.get(
157 order_by = request.GET.get(
158 'columns[%s][data][sort]' % column_index, 'name_raw')
158 'columns[%s][data][sort]' % column_index, 'name_raw')
159 return order_by, order_dir
159 return order_by, order_dir
160
160
161 @LoginRequired()
161 @LoginRequired()
162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
163 'repository.admin')
163 'repository.admin')
164 @HasAcceptedRepoType('git', 'hg')
164 @HasAcceptedRepoType('git', 'hg')
165 def show_all(self, repo_name):
165 def show_all(self, repo_name):
166 # filter types
166 # filter types
167 c.active = 'open'
167 c.active = 'open'
168 c.source = str2bool(request.GET.get('source'))
168 c.source = str2bool(request.GET.get('source'))
169 c.closed = str2bool(request.GET.get('closed'))
169 c.closed = str2bool(request.GET.get('closed'))
170 c.my = str2bool(request.GET.get('my'))
170 c.my = str2bool(request.GET.get('my'))
171 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
171 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
172 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
172 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
173 c.repo_name = repo_name
173 c.repo_name = repo_name
174
174
175 opened_by = None
175 opened_by = None
176 if c.my:
176 if c.my:
177 c.active = 'my'
177 c.active = 'my'
178 opened_by = [c.rhodecode_user.user_id]
178 opened_by = [c.rhodecode_user.user_id]
179
179
180 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
180 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
181 if c.closed:
181 if c.closed:
182 c.active = 'closed'
182 c.active = 'closed'
183 statuses = [PullRequest.STATUS_CLOSED]
183 statuses = [PullRequest.STATUS_CLOSED]
184
184
185 if c.awaiting_review and not c.source:
185 if c.awaiting_review and not c.source:
186 c.active = 'awaiting'
186 c.active = 'awaiting'
187 if c.source and not c.awaiting_review:
187 if c.source and not c.awaiting_review:
188 c.active = 'source'
188 c.active = 'source'
189 if c.awaiting_my_review:
189 if c.awaiting_my_review:
190 c.active = 'awaiting_my'
190 c.active = 'awaiting_my'
191
191
192 data = self._get_pull_requests_list(
192 data = self._get_pull_requests_list(
193 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
193 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
194 if not request.is_xhr:
194 if not request.is_xhr:
195 c.data = json.dumps(data['data'])
195 c.data = json.dumps(data['data'])
196 c.records_total = data['recordsTotal']
196 c.records_total = data['recordsTotal']
197 return render('/pullrequests/pullrequests.html')
197 return render('/pullrequests/pullrequests.html')
198 else:
198 else:
199 return json.dumps(data)
199 return json.dumps(data)
200
200
201 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
201 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
202 # pagination
202 # pagination
203 start = safe_int(request.GET.get('start'), 0)
203 start = safe_int(request.GET.get('start'), 0)
204 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
204 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
205 order_by, order_dir = self._extract_ordering(request)
205 order_by, order_dir = self._extract_ordering(request)
206
206
207 if c.awaiting_review:
207 if c.awaiting_review:
208 pull_requests = PullRequestModel().get_awaiting_review(
208 pull_requests = PullRequestModel().get_awaiting_review(
209 repo_name, source=c.source, opened_by=opened_by,
209 repo_name, source=c.source, opened_by=opened_by,
210 statuses=statuses, offset=start, length=length,
210 statuses=statuses, offset=start, length=length,
211 order_by=order_by, order_dir=order_dir)
211 order_by=order_by, order_dir=order_dir)
212 pull_requests_total_count = PullRequestModel(
212 pull_requests_total_count = PullRequestModel(
213 ).count_awaiting_review(
213 ).count_awaiting_review(
214 repo_name, source=c.source, statuses=statuses,
214 repo_name, source=c.source, statuses=statuses,
215 opened_by=opened_by)
215 opened_by=opened_by)
216 elif c.awaiting_my_review:
216 elif c.awaiting_my_review:
217 pull_requests = PullRequestModel().get_awaiting_my_review(
217 pull_requests = PullRequestModel().get_awaiting_my_review(
218 repo_name, source=c.source, opened_by=opened_by,
218 repo_name, source=c.source, opened_by=opened_by,
219 user_id=c.rhodecode_user.user_id, statuses=statuses,
219 user_id=c.rhodecode_user.user_id, statuses=statuses,
220 offset=start, length=length, order_by=order_by,
220 offset=start, length=length, order_by=order_by,
221 order_dir=order_dir)
221 order_dir=order_dir)
222 pull_requests_total_count = PullRequestModel(
222 pull_requests_total_count = PullRequestModel(
223 ).count_awaiting_my_review(
223 ).count_awaiting_my_review(
224 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
224 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
225 statuses=statuses, opened_by=opened_by)
225 statuses=statuses, opened_by=opened_by)
226 else:
226 else:
227 pull_requests = PullRequestModel().get_all(
227 pull_requests = PullRequestModel().get_all(
228 repo_name, source=c.source, opened_by=opened_by,
228 repo_name, source=c.source, opened_by=opened_by,
229 statuses=statuses, offset=start, length=length,
229 statuses=statuses, offset=start, length=length,
230 order_by=order_by, order_dir=order_dir)
230 order_by=order_by, order_dir=order_dir)
231 pull_requests_total_count = PullRequestModel().count_all(
231 pull_requests_total_count = PullRequestModel().count_all(
232 repo_name, source=c.source, statuses=statuses,
232 repo_name, source=c.source, statuses=statuses,
233 opened_by=opened_by)
233 opened_by=opened_by)
234
234
235 from rhodecode.lib.utils import PartialRenderer
235 from rhodecode.lib.utils import PartialRenderer
236 _render = PartialRenderer('data_table/_dt_elements.html')
236 _render = PartialRenderer('data_table/_dt_elements.html')
237 data = []
237 data = []
238 for pr in pull_requests:
238 for pr in pull_requests:
239 comments = ChangesetCommentsModel().get_all_comments(
239 comments = ChangesetCommentsModel().get_all_comments(
240 c.rhodecode_db_repo.repo_id, pull_request=pr)
240 c.rhodecode_db_repo.repo_id, pull_request=pr)
241
241
242 data.append({
242 data.append({
243 'name': _render('pullrequest_name',
243 'name': _render('pullrequest_name',
244 pr.pull_request_id, pr.target_repo.repo_name),
244 pr.pull_request_id, pr.target_repo.repo_name),
245 'name_raw': pr.pull_request_id,
245 'name_raw': pr.pull_request_id,
246 'status': _render('pullrequest_status',
246 'status': _render('pullrequest_status',
247 pr.calculated_review_status()),
247 pr.calculated_review_status()),
248 'title': _render(
248 'title': _render(
249 'pullrequest_title', pr.title, pr.description),
249 'pullrequest_title', pr.title, pr.description),
250 'description': h.escape(pr.description),
250 'description': h.escape(pr.description),
251 'updated_on': _render('pullrequest_updated_on',
251 'updated_on': _render('pullrequest_updated_on',
252 h.datetime_to_time(pr.updated_on)),
252 h.datetime_to_time(pr.updated_on)),
253 'updated_on_raw': h.datetime_to_time(pr.updated_on),
253 'updated_on_raw': h.datetime_to_time(pr.updated_on),
254 'created_on': _render('pullrequest_updated_on',
254 'created_on': _render('pullrequest_updated_on',
255 h.datetime_to_time(pr.created_on)),
255 h.datetime_to_time(pr.created_on)),
256 'created_on_raw': h.datetime_to_time(pr.created_on),
256 'created_on_raw': h.datetime_to_time(pr.created_on),
257 'author': _render('pullrequest_author',
257 'author': _render('pullrequest_author',
258 pr.author.full_contact, ),
258 pr.author.full_contact, ),
259 'author_raw': pr.author.full_name,
259 'author_raw': pr.author.full_name,
260 'comments': _render('pullrequest_comments', len(comments)),
260 'comments': _render('pullrequest_comments', len(comments)),
261 'comments_raw': len(comments),
261 'comments_raw': len(comments),
262 'closed': pr.is_closed(),
262 'closed': pr.is_closed(),
263 })
263 })
264 # json used to render the grid
264 # json used to render the grid
265 data = ({
265 data = ({
266 'data': data,
266 'data': data,
267 'recordsTotal': pull_requests_total_count,
267 'recordsTotal': pull_requests_total_count,
268 'recordsFiltered': pull_requests_total_count,
268 'recordsFiltered': pull_requests_total_count,
269 })
269 })
270 return data
270 return data
271
271
272 @LoginRequired()
272 @LoginRequired()
273 @NotAnonymous()
273 @NotAnonymous()
274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
275 'repository.admin')
275 'repository.admin')
276 @HasAcceptedRepoType('git', 'hg')
276 @HasAcceptedRepoType('git', 'hg')
277 def index(self):
277 def index(self):
278 source_repo = c.rhodecode_db_repo
278 source_repo = c.rhodecode_db_repo
279
279
280 try:
280 try:
281 source_repo.scm_instance().get_commit()
281 source_repo.scm_instance().get_commit()
282 except EmptyRepositoryError:
282 except EmptyRepositoryError:
283 h.flash(h.literal(_('There are no commits yet')),
283 h.flash(h.literal(_('There are no commits yet')),
284 category='warning')
284 category='warning')
285 redirect(url('summary_home', repo_name=source_repo.repo_name))
285 redirect(url('summary_home', repo_name=source_repo.repo_name))
286
286
287 commit_id = request.GET.get('commit')
287 commit_id = request.GET.get('commit')
288 branch_ref = request.GET.get('branch')
288 branch_ref = request.GET.get('branch')
289 bookmark_ref = request.GET.get('bookmark')
289 bookmark_ref = request.GET.get('bookmark')
290
290
291 try:
291 try:
292 source_repo_data = PullRequestModel().generate_repo_data(
292 source_repo_data = PullRequestModel().generate_repo_data(
293 source_repo, commit_id=commit_id,
293 source_repo, commit_id=commit_id,
294 branch=branch_ref, bookmark=bookmark_ref)
294 branch=branch_ref, bookmark=bookmark_ref)
295 except CommitDoesNotExistError as e:
295 except CommitDoesNotExistError as e:
296 log.exception(e)
296 log.exception(e)
297 h.flash(_('Commit does not exist'), 'error')
297 h.flash(_('Commit does not exist'), 'error')
298 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
298 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
299
299
300 default_target_repo = source_repo
300 default_target_repo = source_repo
301
301
302 if source_repo.parent:
302 if source_repo.parent:
303 parent_vcs_obj = source_repo.parent.scm_instance()
303 parent_vcs_obj = source_repo.parent.scm_instance()
304 if parent_vcs_obj and not parent_vcs_obj.is_empty():
304 if parent_vcs_obj and not parent_vcs_obj.is_empty():
305 # change default if we have a parent repo
305 # change default if we have a parent repo
306 default_target_repo = source_repo.parent
306 default_target_repo = source_repo.parent
307
307
308 target_repo_data = PullRequestModel().generate_repo_data(
308 target_repo_data = PullRequestModel().generate_repo_data(
309 default_target_repo)
309 default_target_repo)
310
310
311 selected_source_ref = source_repo_data['refs']['selected_ref']
311 selected_source_ref = source_repo_data['refs']['selected_ref']
312
312
313 title_source_ref = selected_source_ref.split(':', 2)[1]
313 title_source_ref = selected_source_ref.split(':', 2)[1]
314 c.default_title = PullRequestModel().generate_pullrequest_title(
314 c.default_title = PullRequestModel().generate_pullrequest_title(
315 source=source_repo.repo_name,
315 source=source_repo.repo_name,
316 source_ref=title_source_ref,
316 source_ref=title_source_ref,
317 target=default_target_repo.repo_name
317 target=default_target_repo.repo_name
318 )
318 )
319
319
320 c.default_repo_data = {
320 c.default_repo_data = {
321 'source_repo_name': source_repo.repo_name,
321 'source_repo_name': source_repo.repo_name,
322 'source_refs_json': json.dumps(source_repo_data),
322 'source_refs_json': json.dumps(source_repo_data),
323 'target_repo_name': default_target_repo.repo_name,
323 'target_repo_name': default_target_repo.repo_name,
324 'target_refs_json': json.dumps(target_repo_data),
324 'target_refs_json': json.dumps(target_repo_data),
325 }
325 }
326 c.default_source_ref = selected_source_ref
326 c.default_source_ref = selected_source_ref
327
327
328 return render('/pullrequests/pullrequest.html')
328 return render('/pullrequests/pullrequest.html')
329
329
330 @LoginRequired()
330 @LoginRequired()
331 @NotAnonymous()
331 @NotAnonymous()
332 @XHRRequired()
332 @XHRRequired()
333 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
333 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
334 'repository.admin')
334 'repository.admin')
335 @jsonify
335 @jsonify
336 def get_repo_refs(self, repo_name, target_repo_name):
336 def get_repo_refs(self, repo_name, target_repo_name):
337 repo = Repository.get_by_repo_name(target_repo_name)
337 repo = Repository.get_by_repo_name(target_repo_name)
338 if not repo:
338 if not repo:
339 raise HTTPNotFound
339 raise HTTPNotFound
340 return PullRequestModel().generate_repo_data(repo)
340 return PullRequestModel().generate_repo_data(repo)
341
341
342 @LoginRequired()
342 @LoginRequired()
343 @NotAnonymous()
343 @NotAnonymous()
344 @XHRRequired()
344 @XHRRequired()
345 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
345 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
346 'repository.admin')
346 'repository.admin')
347 @jsonify
347 @jsonify
348 def get_repo_destinations(self, repo_name):
348 def get_repo_destinations(self, repo_name):
349 repo = Repository.get_by_repo_name(repo_name)
349 repo = Repository.get_by_repo_name(repo_name)
350 if not repo:
350 if not repo:
351 raise HTTPNotFound
351 raise HTTPNotFound
352 filter_query = request.GET.get('query')
352 filter_query = request.GET.get('query')
353
353
354 query = Repository.query() \
354 query = Repository.query() \
355 .order_by(func.length(Repository.repo_name)) \
355 .order_by(func.length(Repository.repo_name)) \
356 .filter(or_(
356 .filter(or_(
357 Repository.repo_name == repo.repo_name,
357 Repository.repo_name == repo.repo_name,
358 Repository.fork_id == repo.repo_id))
358 Repository.fork_id == repo.repo_id))
359
359
360 if filter_query:
360 if filter_query:
361 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
361 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
362 query = query.filter(
362 query = query.filter(
363 Repository.repo_name.ilike(ilike_expression))
363 Repository.repo_name.ilike(ilike_expression))
364
364
365 add_parent = False
365 add_parent = False
366 if repo.parent:
366 if repo.parent:
367 if filter_query in repo.parent.repo_name:
367 if filter_query in repo.parent.repo_name:
368 parent_vcs_obj = repo.parent.scm_instance()
368 parent_vcs_obj = repo.parent.scm_instance()
369 if parent_vcs_obj and not parent_vcs_obj.is_empty():
369 if parent_vcs_obj and not parent_vcs_obj.is_empty():
370 add_parent = True
370 add_parent = True
371
371
372 limit = 20 - 1 if add_parent else 20
372 limit = 20 - 1 if add_parent else 20
373 all_repos = query.limit(limit).all()
373 all_repos = query.limit(limit).all()
374 if add_parent:
374 if add_parent:
375 all_repos += [repo.parent]
375 all_repos += [repo.parent]
376
376
377 repos = []
377 repos = []
378 for obj in self.scm_model.get_repos(all_repos):
378 for obj in self.scm_model.get_repos(all_repos):
379 repos.append({
379 repos.append({
380 'id': obj['name'],
380 'id': obj['name'],
381 'text': obj['name'],
381 'text': obj['name'],
382 'type': 'repo',
382 'type': 'repo',
383 'obj': obj['dbrepo']
383 'obj': obj['dbrepo']
384 })
384 })
385
385
386 data = {
386 data = {
387 'more': False,
387 'more': False,
388 'results': [{
388 'results': [{
389 'text': _('Repositories'),
389 'text': _('Repositories'),
390 'children': repos
390 'children': repos
391 }] if repos else []
391 }] if repos else []
392 }
392 }
393 return data
393 return data
394
394
395 @LoginRequired()
395 @LoginRequired()
396 @NotAnonymous()
396 @NotAnonymous()
397 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
397 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
398 'repository.admin')
398 'repository.admin')
399 @HasAcceptedRepoType('git', 'hg')
399 @HasAcceptedRepoType('git', 'hg')
400 @auth.CSRFRequired()
400 @auth.CSRFRequired()
401 def create(self, repo_name):
401 def create(self, repo_name):
402 repo = Repository.get_by_repo_name(repo_name)
402 repo = Repository.get_by_repo_name(repo_name)
403 if not repo:
403 if not repo:
404 raise HTTPNotFound
404 raise HTTPNotFound
405
405
406 controls = peppercorn.parse(request.POST.items())
406 controls = peppercorn.parse(request.POST.items())
407
407
408 try:
408 try:
409 _form = PullRequestForm(repo.repo_id)().to_python(controls)
409 _form = PullRequestForm(repo.repo_id)().to_python(controls)
410 except formencode.Invalid as errors:
410 except formencode.Invalid as errors:
411 if errors.error_dict.get('revisions'):
411 if errors.error_dict.get('revisions'):
412 msg = 'Revisions: %s' % errors.error_dict['revisions']
412 msg = 'Revisions: %s' % errors.error_dict['revisions']
413 elif errors.error_dict.get('pullrequest_title'):
413 elif errors.error_dict.get('pullrequest_title'):
414 msg = _('Pull request requires a title with min. 3 chars')
414 msg = _('Pull request requires a title with min. 3 chars')
415 else:
415 else:
416 msg = _('Error creating pull request: {}').format(errors)
416 msg = _('Error creating pull request: {}').format(errors)
417 log.exception(msg)
417 log.exception(msg)
418 h.flash(msg, 'error')
418 h.flash(msg, 'error')
419
419
420 # would rather just go back to form ...
420 # would rather just go back to form ...
421 return redirect(url('pullrequest_home', repo_name=repo_name))
421 return redirect(url('pullrequest_home', repo_name=repo_name))
422
422
423 source_repo = _form['source_repo']
423 source_repo = _form['source_repo']
424 source_ref = _form['source_ref']
424 source_ref = _form['source_ref']
425 target_repo = _form['target_repo']
425 target_repo = _form['target_repo']
426 target_ref = _form['target_ref']
426 target_ref = _form['target_ref']
427 commit_ids = _form['revisions'][::-1]
427 commit_ids = _form['revisions'][::-1]
428 reviewers = [
428 reviewers = [
429 (r['user_id'], r['reasons']) for r in _form['review_members']]
429 (r['user_id'], r['reasons']) for r in _form['review_members']]
430
430
431 # find the ancestor for this pr
431 # find the ancestor for this pr
432 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
432 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
433 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
433 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
434
434
435 source_scm = source_db_repo.scm_instance()
435 source_scm = source_db_repo.scm_instance()
436 target_scm = target_db_repo.scm_instance()
436 target_scm = target_db_repo.scm_instance()
437
437
438 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
438 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
439 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
439 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
440
440
441 ancestor = source_scm.get_common_ancestor(
441 ancestor = source_scm.get_common_ancestor(
442 source_commit.raw_id, target_commit.raw_id, target_scm)
442 source_commit.raw_id, target_commit.raw_id, target_scm)
443
443
444 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
444 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
445 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
445 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
446
446
447 pullrequest_title = _form['pullrequest_title']
447 pullrequest_title = _form['pullrequest_title']
448 title_source_ref = source_ref.split(':', 2)[1]
448 title_source_ref = source_ref.split(':', 2)[1]
449 if not pullrequest_title:
449 if not pullrequest_title:
450 pullrequest_title = PullRequestModel().generate_pullrequest_title(
450 pullrequest_title = PullRequestModel().generate_pullrequest_title(
451 source=source_repo,
451 source=source_repo,
452 source_ref=title_source_ref,
452 source_ref=title_source_ref,
453 target=target_repo
453 target=target_repo
454 )
454 )
455
455
456 description = _form['pullrequest_desc']
456 description = _form['pullrequest_desc']
457 try:
457 try:
458 pull_request = PullRequestModel().create(
458 pull_request = PullRequestModel().create(
459 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
459 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
460 target_ref, commit_ids, reviewers, pullrequest_title,
460 target_ref, commit_ids, reviewers, pullrequest_title,
461 description
461 description
462 )
462 )
463 Session().commit()
463 Session().commit()
464 h.flash(_('Successfully opened new pull request'),
464 h.flash(_('Successfully opened new pull request'),
465 category='success')
465 category='success')
466 except Exception as e:
466 except Exception as e:
467 msg = _('Error occurred during sending pull request')
467 msg = _('Error occurred during sending pull request')
468 log.exception(msg)
468 log.exception(msg)
469 h.flash(msg, category='error')
469 h.flash(msg, category='error')
470 return redirect(url('pullrequest_home', repo_name=repo_name))
470 return redirect(url('pullrequest_home', repo_name=repo_name))
471
471
472 return redirect(url('pullrequest_show', repo_name=target_repo,
472 return redirect(url('pullrequest_show', repo_name=target_repo,
473 pull_request_id=pull_request.pull_request_id))
473 pull_request_id=pull_request.pull_request_id))
474
474
475 @LoginRequired()
475 @LoginRequired()
476 @NotAnonymous()
476 @NotAnonymous()
477 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
477 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
478 'repository.admin')
478 'repository.admin')
479 @auth.CSRFRequired()
479 @auth.CSRFRequired()
480 @jsonify
480 @jsonify
481 def update(self, repo_name, pull_request_id):
481 def update(self, repo_name, pull_request_id):
482 pull_request_id = safe_int(pull_request_id)
482 pull_request_id = safe_int(pull_request_id)
483 pull_request = PullRequest.get_or_404(pull_request_id)
483 pull_request = PullRequest.get_or_404(pull_request_id)
484 # only owner or admin can update it
484 # only owner or admin can update it
485 allowed_to_update = PullRequestModel().check_user_update(
485 allowed_to_update = PullRequestModel().check_user_update(
486 pull_request, c.rhodecode_user)
486 pull_request, c.rhodecode_user)
487 if allowed_to_update:
487 if allowed_to_update:
488 controls = peppercorn.parse(request.POST.items())
488 controls = peppercorn.parse(request.POST.items())
489
489
490 if 'review_members' in controls:
490 if 'review_members' in controls:
491 self._update_reviewers(
491 self._update_reviewers(
492 pull_request_id, controls['review_members'])
492 pull_request_id, controls['review_members'])
493 elif str2bool(request.POST.get('update_commits', 'false')):
493 elif str2bool(request.POST.get('update_commits', 'false')):
494 self._update_commits(pull_request)
494 self._update_commits(pull_request)
495 elif str2bool(request.POST.get('close_pull_request', 'false')):
495 elif str2bool(request.POST.get('close_pull_request', 'false')):
496 self._reject_close(pull_request)
496 self._reject_close(pull_request)
497 elif str2bool(request.POST.get('edit_pull_request', 'false')):
497 elif str2bool(request.POST.get('edit_pull_request', 'false')):
498 self._edit_pull_request(pull_request)
498 self._edit_pull_request(pull_request)
499 else:
499 else:
500 raise HTTPBadRequest()
500 raise HTTPBadRequest()
501 return True
501 return True
502 raise HTTPForbidden()
502 raise HTTPForbidden()
503
503
504 def _edit_pull_request(self, pull_request):
504 def _edit_pull_request(self, pull_request):
505 try:
505 try:
506 PullRequestModel().edit(
506 PullRequestModel().edit(
507 pull_request, request.POST.get('title'),
507 pull_request, request.POST.get('title'),
508 request.POST.get('description'))
508 request.POST.get('description'))
509 except ValueError:
509 except ValueError:
510 msg = _(u'Cannot update closed pull requests.')
510 msg = _(u'Cannot update closed pull requests.')
511 h.flash(msg, category='error')
511 h.flash(msg, category='error')
512 return
512 return
513 else:
513 else:
514 Session().commit()
514 Session().commit()
515
515
516 msg = _(u'Pull request title & description updated.')
516 msg = _(u'Pull request title & description updated.')
517 h.flash(msg, category='success')
517 h.flash(msg, category='success')
518 return
518 return
519
519
520 def _update_commits(self, pull_request):
520 def _update_commits(self, pull_request):
521 resp = PullRequestModel().update_commits(pull_request)
521 resp = PullRequestModel().update_commits(pull_request)
522 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
523
522
524 # Abort if pull request update failed.
523 if resp.success:
525 if not resp.success:
526 h.flash(msg, category='error')
527 return
528
529 if resp.reason == UpdateFailureReason.NONE:
530 msg = _(
524 msg = _(
531 u'Pull request updated to "{source_commit_id}" with '
525 u'Pull request updated to "{source_commit_id}" with '
532 u'{count_added} added, {count_removed} removed commits.')
526 u'{count_added} added, {count_removed} removed commits.')
533 msg = msg.format(
527 msg = msg.format(
534 source_commit_id=pull_request.source_ref_parts.commit_id,
528 source_commit_id=pull_request.source_ref_parts.commit_id,
535 count_added=len(resp.changes.added),
529 count_added=len(resp.changes.added),
536 count_removed=len(resp.changes.removed))
530 count_removed=len(resp.changes.removed))
537 h.flash(msg, category='success')
531 h.flash(msg, category='success')
538
532
539 registry = get_current_registry()
533 registry = get_current_registry()
540 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
534 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
541 channelstream_config = rhodecode_plugins.get('channelstream', {})
535 channelstream_config = rhodecode_plugins.get('channelstream', {})
542 if channelstream_config.get('enabled'):
536 if channelstream_config.get('enabled'):
543 message = msg + (
537 message = msg + (
544 ' - <a onclick="window.location.reload()">'
538 ' - <a onclick="window.location.reload()">'
545 '<strong>{}</strong></a>'.format(_('Reload page')))
539 '<strong>{}</strong></a>'.format(_('Reload page')))
546 channel = '/repo${}$/pr/{}'.format(
540 channel = '/repo${}$/pr/{}'.format(
547 pull_request.target_repo.repo_name,
541 pull_request.target_repo.repo_name,
548 pull_request.pull_request_id
542 pull_request.pull_request_id
549 )
543 )
550 payload = {
544 payload = {
551 'type': 'message',
545 'type': 'message',
552 'user': 'system',
546 'user': 'system',
553 'exclude_users': [request.user.username],
547 'exclude_users': [request.user.username],
554 'channel': channel,
548 'channel': channel,
555 'message': {
549 'message': {
556 'message': message,
550 'message': message,
557 'level': 'success',
551 'level': 'success',
558 'topic': '/notifications'
552 'topic': '/notifications'
559 }
553 }
560 }
554 }
561 channelstream_request(
555 channelstream_request(
562 channelstream_config, [payload], '/message',
556 channelstream_config, [payload], '/message',
563 raise_exc=False)
557 raise_exc=False)
564 elif resp.reason == UpdateFailureReason.NO_CHANGE:
565 # Display a warning if no update is needed.
566 h.flash(msg, category='warning')
567 else:
558 else:
568 h.flash(msg, category='error')
559 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
560 warning_reasons = [
561 UpdateFailureReason.NO_CHANGE,
562 UpdateFailureReason.WRONG_REF_TPYE,
563 ]
564 category = 'warning' if resp.reason in warning_reasons else 'error'
565 h.flash(msg, category=category)
569
566
570 @auth.CSRFRequired()
567 @auth.CSRFRequired()
571 @LoginRequired()
568 @LoginRequired()
572 @NotAnonymous()
569 @NotAnonymous()
573 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
570 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
574 'repository.admin')
571 'repository.admin')
575 def merge(self, repo_name, pull_request_id):
572 def merge(self, repo_name, pull_request_id):
576 """
573 """
577 POST /{repo_name}/pull-request/{pull_request_id}
574 POST /{repo_name}/pull-request/{pull_request_id}
578
575
579 Merge will perform a server-side merge of the specified
576 Merge will perform a server-side merge of the specified
580 pull request, if the pull request is approved and mergeable.
577 pull request, if the pull request is approved and mergeable.
581 After succesfull merging, the pull request is automatically
578 After succesfull merging, the pull request is automatically
582 closed, with a relevant comment.
579 closed, with a relevant comment.
583 """
580 """
584 pull_request_id = safe_int(pull_request_id)
581 pull_request_id = safe_int(pull_request_id)
585 pull_request = PullRequest.get_or_404(pull_request_id)
582 pull_request = PullRequest.get_or_404(pull_request_id)
586 user = c.rhodecode_user
583 user = c.rhodecode_user
587
584
588 if self._meets_merge_pre_conditions(pull_request, user):
585 if self._meets_merge_pre_conditions(pull_request, user):
589 log.debug("Pre-conditions checked, trying to merge.")
586 log.debug("Pre-conditions checked, trying to merge.")
590 extras = vcs_operation_context(
587 extras = vcs_operation_context(
591 request.environ, repo_name=pull_request.target_repo.repo_name,
588 request.environ, repo_name=pull_request.target_repo.repo_name,
592 username=user.username, action='push',
589 username=user.username, action='push',
593 scm=pull_request.target_repo.repo_type)
590 scm=pull_request.target_repo.repo_type)
594 self._merge_pull_request(pull_request, user, extras)
591 self._merge_pull_request(pull_request, user, extras)
595
592
596 return redirect(url(
593 return redirect(url(
597 'pullrequest_show',
594 'pullrequest_show',
598 repo_name=pull_request.target_repo.repo_name,
595 repo_name=pull_request.target_repo.repo_name,
599 pull_request_id=pull_request.pull_request_id))
596 pull_request_id=pull_request.pull_request_id))
600
597
601 def _meets_merge_pre_conditions(self, pull_request, user):
598 def _meets_merge_pre_conditions(self, pull_request, user):
602 if not PullRequestModel().check_user_merge(pull_request, user):
599 if not PullRequestModel().check_user_merge(pull_request, user):
603 raise HTTPForbidden()
600 raise HTTPForbidden()
604
601
605 merge_status, msg = PullRequestModel().merge_status(pull_request)
602 merge_status, msg = PullRequestModel().merge_status(pull_request)
606 if not merge_status:
603 if not merge_status:
607 log.debug("Cannot merge, not mergeable.")
604 log.debug("Cannot merge, not mergeable.")
608 h.flash(msg, category='error')
605 h.flash(msg, category='error')
609 return False
606 return False
610
607
611 if (pull_request.calculated_review_status()
608 if (pull_request.calculated_review_status()
612 is not ChangesetStatus.STATUS_APPROVED):
609 is not ChangesetStatus.STATUS_APPROVED):
613 log.debug("Cannot merge, approval is pending.")
610 log.debug("Cannot merge, approval is pending.")
614 msg = _('Pull request reviewer approval is pending.')
611 msg = _('Pull request reviewer approval is pending.')
615 h.flash(msg, category='error')
612 h.flash(msg, category='error')
616 return False
613 return False
617 return True
614 return True
618
615
619 def _merge_pull_request(self, pull_request, user, extras):
616 def _merge_pull_request(self, pull_request, user, extras):
620 merge_resp = PullRequestModel().merge(
617 merge_resp = PullRequestModel().merge(
621 pull_request, user, extras=extras)
618 pull_request, user, extras=extras)
622
619
623 if merge_resp.executed:
620 if merge_resp.executed:
624 log.debug("The merge was successful, closing the pull request.")
621 log.debug("The merge was successful, closing the pull request.")
625 PullRequestModel().close_pull_request(
622 PullRequestModel().close_pull_request(
626 pull_request.pull_request_id, user)
623 pull_request.pull_request_id, user)
627 Session().commit()
624 Session().commit()
628 msg = _('Pull request was successfully merged and closed.')
625 msg = _('Pull request was successfully merged and closed.')
629 h.flash(msg, category='success')
626 h.flash(msg, category='success')
630 else:
627 else:
631 log.debug(
628 log.debug(
632 "The merge was not successful. Merge response: %s",
629 "The merge was not successful. Merge response: %s",
633 merge_resp)
630 merge_resp)
634 msg = PullRequestModel().merge_status_message(
631 msg = PullRequestModel().merge_status_message(
635 merge_resp.failure_reason)
632 merge_resp.failure_reason)
636 h.flash(msg, category='error')
633 h.flash(msg, category='error')
637
634
638 def _update_reviewers(self, pull_request_id, review_members):
635 def _update_reviewers(self, pull_request_id, review_members):
639 reviewers = [
636 reviewers = [
640 (int(r['user_id']), r['reasons']) for r in review_members]
637 (int(r['user_id']), r['reasons']) for r in review_members]
641 PullRequestModel().update_reviewers(pull_request_id, reviewers)
638 PullRequestModel().update_reviewers(pull_request_id, reviewers)
642 Session().commit()
639 Session().commit()
643
640
644 def _reject_close(self, pull_request):
641 def _reject_close(self, pull_request):
645 if pull_request.is_closed():
642 if pull_request.is_closed():
646 raise HTTPForbidden()
643 raise HTTPForbidden()
647
644
648 PullRequestModel().close_pull_request_with_comment(
645 PullRequestModel().close_pull_request_with_comment(
649 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
646 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
650 Session().commit()
647 Session().commit()
651
648
652 @LoginRequired()
649 @LoginRequired()
653 @NotAnonymous()
650 @NotAnonymous()
654 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
651 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
655 'repository.admin')
652 'repository.admin')
656 @auth.CSRFRequired()
653 @auth.CSRFRequired()
657 @jsonify
654 @jsonify
658 def delete(self, repo_name, pull_request_id):
655 def delete(self, repo_name, pull_request_id):
659 pull_request_id = safe_int(pull_request_id)
656 pull_request_id = safe_int(pull_request_id)
660 pull_request = PullRequest.get_or_404(pull_request_id)
657 pull_request = PullRequest.get_or_404(pull_request_id)
661 # only owner can delete it !
658 # only owner can delete it !
662 if pull_request.author.user_id == c.rhodecode_user.user_id:
659 if pull_request.author.user_id == c.rhodecode_user.user_id:
663 PullRequestModel().delete(pull_request)
660 PullRequestModel().delete(pull_request)
664 Session().commit()
661 Session().commit()
665 h.flash(_('Successfully deleted pull request'),
662 h.flash(_('Successfully deleted pull request'),
666 category='success')
663 category='success')
667 return redirect(url('my_account_pullrequests'))
664 return redirect(url('my_account_pullrequests'))
668 raise HTTPForbidden()
665 raise HTTPForbidden()
669
666
670 @LoginRequired()
667 @LoginRequired()
671 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
668 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
672 'repository.admin')
669 'repository.admin')
673 def show(self, repo_name, pull_request_id):
670 def show(self, repo_name, pull_request_id):
674 pull_request_id = safe_int(pull_request_id)
671 pull_request_id = safe_int(pull_request_id)
675 c.pull_request = PullRequest.get_or_404(pull_request_id)
672 c.pull_request = PullRequest.get_or_404(pull_request_id)
676
673
677 c.template_context['pull_request_data']['pull_request_id'] = \
674 c.template_context['pull_request_data']['pull_request_id'] = \
678 pull_request_id
675 pull_request_id
679
676
680 # pull_requests repo_name we opened it against
677 # pull_requests repo_name we opened it against
681 # ie. target_repo must match
678 # ie. target_repo must match
682 if repo_name != c.pull_request.target_repo.repo_name:
679 if repo_name != c.pull_request.target_repo.repo_name:
683 raise HTTPNotFound
680 raise HTTPNotFound
684
681
685 c.allowed_to_change_status = PullRequestModel(). \
682 c.allowed_to_change_status = PullRequestModel(). \
686 check_user_change_status(c.pull_request, c.rhodecode_user)
683 check_user_change_status(c.pull_request, c.rhodecode_user)
687 c.allowed_to_update = PullRequestModel().check_user_update(
684 c.allowed_to_update = PullRequestModel().check_user_update(
688 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
685 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
689 c.allowed_to_merge = PullRequestModel().check_user_merge(
686 c.allowed_to_merge = PullRequestModel().check_user_merge(
690 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
687 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
691 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
688 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
692 c.pull_request)
689 c.pull_request)
693
690
694 cc_model = ChangesetCommentsModel()
691 cc_model = ChangesetCommentsModel()
695
692
696 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
693 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
697
694
698 c.pull_request_review_status = c.pull_request.calculated_review_status()
695 c.pull_request_review_status = c.pull_request.calculated_review_status()
699 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
696 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
700 c.pull_request)
697 c.pull_request)
701 c.approval_msg = None
698 c.approval_msg = None
702 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
699 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
703 c.approval_msg = _('Reviewer approval is pending.')
700 c.approval_msg = _('Reviewer approval is pending.')
704 c.pr_merge_status = False
701 c.pr_merge_status = False
705 # load compare data into template context
702 # load compare data into template context
706 enable_comments = not c.pull_request.is_closed()
703 enable_comments = not c.pull_request.is_closed()
707 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
704 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
708
705
709 # this is a hack to properly display links, when creating PR, the
706 # this is a hack to properly display links, when creating PR, the
710 # compare view and others uses different notation, and
707 # compare view and others uses different notation, and
711 # compare_commits.html renders links based on the target_repo.
708 # compare_commits.html renders links based on the target_repo.
712 # We need to swap that here to generate it properly on the html side
709 # We need to swap that here to generate it properly on the html side
713 c.target_repo = c.source_repo
710 c.target_repo = c.source_repo
714
711
715 # inline comments
712 # inline comments
716 c.inline_cnt = 0
713 c.inline_cnt = 0
717 c.inline_comments = cc_model.get_inline_comments(
714 c.inline_comments = cc_model.get_inline_comments(
718 c.rhodecode_db_repo.repo_id,
715 c.rhodecode_db_repo.repo_id,
719 pull_request=pull_request_id).items()
716 pull_request=pull_request_id).items()
720 # count inline comments
717 # count inline comments
721 for __, lines in c.inline_comments:
718 for __, lines in c.inline_comments:
722 for comments in lines.values():
719 for comments in lines.values():
723 c.inline_cnt += len(comments)
720 c.inline_cnt += len(comments)
724
721
725 # outdated comments
722 # outdated comments
726 c.outdated_cnt = 0
723 c.outdated_cnt = 0
727 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
724 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
728 c.outdated_comments = cc_model.get_outdated_comments(
725 c.outdated_comments = cc_model.get_outdated_comments(
729 c.rhodecode_db_repo.repo_id,
726 c.rhodecode_db_repo.repo_id,
730 pull_request=c.pull_request)
727 pull_request=c.pull_request)
731 # Count outdated comments and check for deleted files
728 # Count outdated comments and check for deleted files
732 for file_name, lines in c.outdated_comments.iteritems():
729 for file_name, lines in c.outdated_comments.iteritems():
733 for comments in lines.values():
730 for comments in lines.values():
734 c.outdated_cnt += len(comments)
731 c.outdated_cnt += len(comments)
735 if file_name not in c.included_files:
732 if file_name not in c.included_files:
736 c.deleted_files.append(file_name)
733 c.deleted_files.append(file_name)
737 else:
734 else:
738 c.outdated_comments = {}
735 c.outdated_comments = {}
739
736
740 # comments
737 # comments
741 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
738 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
742 pull_request=pull_request_id)
739 pull_request=pull_request_id)
743
740
744 if c.allowed_to_update:
741 if c.allowed_to_update:
745 force_close = ('forced_closed', _('Close Pull Request'))
742 force_close = ('forced_closed', _('Close Pull Request'))
746 statuses = ChangesetStatus.STATUSES + [force_close]
743 statuses = ChangesetStatus.STATUSES + [force_close]
747 else:
744 else:
748 statuses = ChangesetStatus.STATUSES
745 statuses = ChangesetStatus.STATUSES
749 c.commit_statuses = statuses
746 c.commit_statuses = statuses
750
747
751 c.ancestor = None # TODO: add ancestor here
748 c.ancestor = None # TODO: add ancestor here
752
749
753 return render('/pullrequests/pullrequest_show.html')
750 return render('/pullrequests/pullrequest_show.html')
754
751
755 @LoginRequired()
752 @LoginRequired()
756 @NotAnonymous()
753 @NotAnonymous()
757 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
754 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
758 'repository.admin')
755 'repository.admin')
759 @auth.CSRFRequired()
756 @auth.CSRFRequired()
760 @jsonify
757 @jsonify
761 def comment(self, repo_name, pull_request_id):
758 def comment(self, repo_name, pull_request_id):
762 pull_request_id = safe_int(pull_request_id)
759 pull_request_id = safe_int(pull_request_id)
763 pull_request = PullRequest.get_or_404(pull_request_id)
760 pull_request = PullRequest.get_or_404(pull_request_id)
764 if pull_request.is_closed():
761 if pull_request.is_closed():
765 raise HTTPForbidden()
762 raise HTTPForbidden()
766
763
767 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
764 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
768 # as a changeset status, still we want to send it in one value.
765 # as a changeset status, still we want to send it in one value.
769 status = request.POST.get('changeset_status', None)
766 status = request.POST.get('changeset_status', None)
770 text = request.POST.get('text')
767 text = request.POST.get('text')
771 if status and '_closed' in status:
768 if status and '_closed' in status:
772 close_pr = True
769 close_pr = True
773 status = status.replace('_closed', '')
770 status = status.replace('_closed', '')
774 else:
771 else:
775 close_pr = False
772 close_pr = False
776
773
777 forced = (status == 'forced')
774 forced = (status == 'forced')
778 if forced:
775 if forced:
779 status = 'rejected'
776 status = 'rejected'
780
777
781 allowed_to_change_status = PullRequestModel().check_user_change_status(
778 allowed_to_change_status = PullRequestModel().check_user_change_status(
782 pull_request, c.rhodecode_user)
779 pull_request, c.rhodecode_user)
783
780
784 if status and allowed_to_change_status:
781 if status and allowed_to_change_status:
785 message = (_('Status change %(transition_icon)s %(status)s')
782 message = (_('Status change %(transition_icon)s %(status)s')
786 % {'transition_icon': '>',
783 % {'transition_icon': '>',
787 'status': ChangesetStatus.get_status_lbl(status)})
784 'status': ChangesetStatus.get_status_lbl(status)})
788 if close_pr:
785 if close_pr:
789 message = _('Closing with') + ' ' + message
786 message = _('Closing with') + ' ' + message
790 text = text or message
787 text = text or message
791 comm = ChangesetCommentsModel().create(
788 comm = ChangesetCommentsModel().create(
792 text=text,
789 text=text,
793 repo=c.rhodecode_db_repo.repo_id,
790 repo=c.rhodecode_db_repo.repo_id,
794 user=c.rhodecode_user.user_id,
791 user=c.rhodecode_user.user_id,
795 pull_request=pull_request_id,
792 pull_request=pull_request_id,
796 f_path=request.POST.get('f_path'),
793 f_path=request.POST.get('f_path'),
797 line_no=request.POST.get('line'),
794 line_no=request.POST.get('line'),
798 status_change=(ChangesetStatus.get_status_lbl(status)
795 status_change=(ChangesetStatus.get_status_lbl(status)
799 if status and allowed_to_change_status else None),
796 if status and allowed_to_change_status else None),
800 status_change_type=(status
797 status_change_type=(status
801 if status and allowed_to_change_status else None),
798 if status and allowed_to_change_status else None),
802 closing_pr=close_pr
799 closing_pr=close_pr
803 )
800 )
804
801
805
802
806
803
807 if allowed_to_change_status:
804 if allowed_to_change_status:
808 old_calculated_status = pull_request.calculated_review_status()
805 old_calculated_status = pull_request.calculated_review_status()
809 # get status if set !
806 # get status if set !
810 if status:
807 if status:
811 ChangesetStatusModel().set_status(
808 ChangesetStatusModel().set_status(
812 c.rhodecode_db_repo.repo_id,
809 c.rhodecode_db_repo.repo_id,
813 status,
810 status,
814 c.rhodecode_user.user_id,
811 c.rhodecode_user.user_id,
815 comm,
812 comm,
816 pull_request=pull_request_id
813 pull_request=pull_request_id
817 )
814 )
818
815
819 Session().flush()
816 Session().flush()
820 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
817 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
821 # we now calculate the status of pull request, and based on that
818 # we now calculate the status of pull request, and based on that
822 # calculation we set the commits status
819 # calculation we set the commits status
823 calculated_status = pull_request.calculated_review_status()
820 calculated_status = pull_request.calculated_review_status()
824 if old_calculated_status != calculated_status:
821 if old_calculated_status != calculated_status:
825 PullRequestModel()._trigger_pull_request_hook(
822 PullRequestModel()._trigger_pull_request_hook(
826 pull_request, c.rhodecode_user, 'review_status_change')
823 pull_request, c.rhodecode_user, 'review_status_change')
827
824
828 calculated_status_lbl = ChangesetStatus.get_status_lbl(
825 calculated_status_lbl = ChangesetStatus.get_status_lbl(
829 calculated_status)
826 calculated_status)
830
827
831 if close_pr:
828 if close_pr:
832 status_completed = (
829 status_completed = (
833 calculated_status in [ChangesetStatus.STATUS_APPROVED,
830 calculated_status in [ChangesetStatus.STATUS_APPROVED,
834 ChangesetStatus.STATUS_REJECTED])
831 ChangesetStatus.STATUS_REJECTED])
835 if forced or status_completed:
832 if forced or status_completed:
836 PullRequestModel().close_pull_request(
833 PullRequestModel().close_pull_request(
837 pull_request_id, c.rhodecode_user)
834 pull_request_id, c.rhodecode_user)
838 else:
835 else:
839 h.flash(_('Closing pull request on other statuses than '
836 h.flash(_('Closing pull request on other statuses than '
840 'rejected or approved is forbidden. '
837 'rejected or approved is forbidden. '
841 'Calculated status from all reviewers '
838 'Calculated status from all reviewers '
842 'is currently: %s') % calculated_status_lbl,
839 'is currently: %s') % calculated_status_lbl,
843 category='warning')
840 category='warning')
844
841
845 Session().commit()
842 Session().commit()
846
843
847 if not request.is_xhr:
844 if not request.is_xhr:
848 return redirect(h.url('pullrequest_show', repo_name=repo_name,
845 return redirect(h.url('pullrequest_show', repo_name=repo_name,
849 pull_request_id=pull_request_id))
846 pull_request_id=pull_request_id))
850
847
851 data = {
848 data = {
852 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
849 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
853 }
850 }
854 if comm:
851 if comm:
855 c.co = comm
852 c.co = comm
856 data.update(comm.get_dict())
853 data.update(comm.get_dict())
857 data.update({'rendered_text':
854 data.update({'rendered_text':
858 render('changeset/changeset_comment_block.html')})
855 render('changeset/changeset_comment_block.html')})
859
856
860 return data
857 return data
861
858
862 @LoginRequired()
859 @LoginRequired()
863 @NotAnonymous()
860 @NotAnonymous()
864 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
861 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
865 'repository.admin')
862 'repository.admin')
866 @auth.CSRFRequired()
863 @auth.CSRFRequired()
867 @jsonify
864 @jsonify
868 def delete_comment(self, repo_name, comment_id):
865 def delete_comment(self, repo_name, comment_id):
869 return self._delete_comment(comment_id)
866 return self._delete_comment(comment_id)
870
867
871 def _delete_comment(self, comment_id):
868 def _delete_comment(self, comment_id):
872 comment_id = safe_int(comment_id)
869 comment_id = safe_int(comment_id)
873 co = ChangesetComment.get_or_404(comment_id)
870 co = ChangesetComment.get_or_404(comment_id)
874 if co.pull_request.is_closed():
871 if co.pull_request.is_closed():
875 # don't allow deleting comments on closed pull request
872 # don't allow deleting comments on closed pull request
876 raise HTTPForbidden()
873 raise HTTPForbidden()
877
874
878 is_owner = co.author.user_id == c.rhodecode_user.user_id
875 is_owner = co.author.user_id == c.rhodecode_user.user_id
879 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
876 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
880 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
877 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
881 old_calculated_status = co.pull_request.calculated_review_status()
878 old_calculated_status = co.pull_request.calculated_review_status()
882 ChangesetCommentsModel().delete(comment=co)
879 ChangesetCommentsModel().delete(comment=co)
883 Session().commit()
880 Session().commit()
884 calculated_status = co.pull_request.calculated_review_status()
881 calculated_status = co.pull_request.calculated_review_status()
885 if old_calculated_status != calculated_status:
882 if old_calculated_status != calculated_status:
886 PullRequestModel()._trigger_pull_request_hook(
883 PullRequestModel()._trigger_pull_request_hook(
887 co.pull_request, c.rhodecode_user, 'review_status_change')
884 co.pull_request, c.rhodecode_user, 'review_status_change')
888 return True
885 return True
889 else:
886 else:
890 raise HTTPForbidden()
887 raise HTTPForbidden()
@@ -1,1250 +1,1250 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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
34
35 from rhodecode.lib import helpers as h, hooks_utils, diffs
35 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib.compat import OrderedDict
36 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
37 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.markup_renderer import (
38 from rhodecode.lib.markup_renderer import (
39 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
39 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 from rhodecode.lib.utils import action_logger
40 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
41 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.vcs.backends.base import (
42 from rhodecode.lib.vcs.backends.base import (
43 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
43 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
44 from rhodecode.lib.vcs.conf import settings as vcs_settings
44 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, EmptyRepositoryError)
46 CommitDoesNotExistError, EmptyRepositoryError)
47 from rhodecode.model import BaseModel
47 from rhodecode.model import BaseModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.db import (
50 from rhodecode.model.db import (
51 PullRequest, PullRequestReviewers, ChangesetStatus,
51 PullRequest, PullRequestReviewers, ChangesetStatus,
52 PullRequestVersion, ChangesetComment)
52 PullRequestVersion, ChangesetComment)
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.notification import NotificationModel, \
54 from rhodecode.model.notification import NotificationModel, \
55 EmailNotificationModel
55 EmailNotificationModel
56 from rhodecode.model.scm import ScmModel
56 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.settings import VcsSettingsModel
57 from rhodecode.model.settings import VcsSettingsModel
58
58
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 # Data structure to hold the response data when updating commits during a pull
63 # Data structure to hold the response data when updating commits during a pull
64 # request update.
64 # request update.
65 UpdateResponse = namedtuple(
65 UpdateResponse = namedtuple(
66 'UpdateResponse', 'success, reason, new, old, changes')
66 'UpdateResponse', 'success, reason, new, old, changes')
67
67
68
68
69 class PullRequestModel(BaseModel):
69 class PullRequestModel(BaseModel):
70
70
71 cls = PullRequest
71 cls = PullRequest
72
72
73 DIFF_CONTEXT = 3
73 DIFF_CONTEXT = 3
74
74
75 MERGE_STATUS_MESSAGES = {
75 MERGE_STATUS_MESSAGES = {
76 MergeFailureReason.NONE: lazy_ugettext(
76 MergeFailureReason.NONE: lazy_ugettext(
77 'This pull request can be automatically merged.'),
77 'This pull request can be automatically merged.'),
78 MergeFailureReason.UNKNOWN: lazy_ugettext(
78 MergeFailureReason.UNKNOWN: lazy_ugettext(
79 'This pull request cannot be merged because of an unhandled'
79 'This pull request cannot be merged because of an unhandled'
80 ' exception.'),
80 ' exception.'),
81 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
81 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
82 'This pull request cannot be merged because of conflicts.'),
82 'This pull request cannot be merged because of conflicts.'),
83 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
83 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
84 'This pull request could not be merged because push to target'
84 'This pull request could not be merged because push to target'
85 ' failed.'),
85 ' failed.'),
86 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
86 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
87 'This pull request cannot be merged because the target is not a'
87 'This pull request cannot be merged because the target is not a'
88 ' head.'),
88 ' head.'),
89 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
89 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
90 'This pull request cannot be merged because the source contains'
90 'This pull request cannot be merged because the source contains'
91 ' more branches than the target.'),
91 ' more branches than the target.'),
92 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
92 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
93 'This pull request cannot be merged because the target has'
93 'This pull request cannot be merged because the target has'
94 ' multiple heads.'),
94 ' multiple heads.'),
95 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
95 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
96 'This pull request cannot be merged because the target repository'
96 'This pull request cannot be merged because the target repository'
97 ' is locked.'),
97 ' is locked.'),
98 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
98 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
99 'This pull request cannot be merged because the target or the '
99 'This pull request cannot be merged because the target or the '
100 'source reference is missing.'),
100 'source reference is missing.'),
101 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
101 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 'This pull request cannot be merged because the target '
102 'This pull request cannot be merged because the target '
103 'reference is missing.'),
103 'reference is missing.'),
104 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
104 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 'This pull request cannot be merged because the source '
105 'This pull request cannot be merged because the source '
106 'reference is missing.'),
106 'reference is missing.'),
107 }
107 }
108
108
109 UPDATE_STATUS_MESSAGES = {
109 UPDATE_STATUS_MESSAGES = {
110 UpdateFailureReason.NONE: lazy_ugettext(
110 UpdateFailureReason.NONE: lazy_ugettext(
111 'Pull request update successful.'),
111 'Pull request update successful.'),
112 UpdateFailureReason.UNKNOWN: lazy_ugettext(
112 UpdateFailureReason.UNKNOWN: lazy_ugettext(
113 'Pull request update failed because of an unknown error.'),
113 'Pull request update failed because of an unknown error.'),
114 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
114 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
115 'No update needed because the source reference is already '
115 'No update needed because the source reference is already '
116 'up to date.'),
116 'up to date.'),
117 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
117 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
118 'Pull request cannot be updated because the reference type is '
118 'Pull request cannot be updated because the reference type is '
119 'not supported for an update.'),
119 'not supported for an update.'),
120 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
120 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
121 'This pull request cannot be updated because the target '
121 'This pull request cannot be updated because the target '
122 'reference is missing.'),
122 'reference is missing.'),
123 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
123 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
124 'This pull request cannot be updated because the source '
124 'This pull request cannot be updated because the source '
125 'reference is missing.'),
125 'reference is missing.'),
126 }
126 }
127
127
128 def __get_pull_request(self, pull_request):
128 def __get_pull_request(self, pull_request):
129 return self._get_instance(PullRequest, pull_request)
129 return self._get_instance(PullRequest, pull_request)
130
130
131 def _check_perms(self, perms, pull_request, user, api=False):
131 def _check_perms(self, perms, pull_request, user, api=False):
132 if not api:
132 if not api:
133 return h.HasRepoPermissionAny(*perms)(
133 return h.HasRepoPermissionAny(*perms)(
134 user=user, repo_name=pull_request.target_repo.repo_name)
134 user=user, repo_name=pull_request.target_repo.repo_name)
135 else:
135 else:
136 return h.HasRepoPermissionAnyApi(*perms)(
136 return h.HasRepoPermissionAnyApi(*perms)(
137 user=user, repo_name=pull_request.target_repo.repo_name)
137 user=user, repo_name=pull_request.target_repo.repo_name)
138
138
139 def check_user_read(self, pull_request, user, api=False):
139 def check_user_read(self, pull_request, user, api=False):
140 _perms = ('repository.admin', 'repository.write', 'repository.read',)
140 _perms = ('repository.admin', 'repository.write', 'repository.read',)
141 return self._check_perms(_perms, pull_request, user, api)
141 return self._check_perms(_perms, pull_request, user, api)
142
142
143 def check_user_merge(self, pull_request, user, api=False):
143 def check_user_merge(self, pull_request, user, api=False):
144 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
144 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
145 return self._check_perms(_perms, pull_request, user, api)
145 return self._check_perms(_perms, pull_request, user, api)
146
146
147 def check_user_update(self, pull_request, user, api=False):
147 def check_user_update(self, pull_request, user, api=False):
148 owner = user.user_id == pull_request.user_id
148 owner = user.user_id == pull_request.user_id
149 return self.check_user_merge(pull_request, user, api) or owner
149 return self.check_user_merge(pull_request, user, api) or owner
150
150
151 def check_user_change_status(self, pull_request, user, api=False):
151 def check_user_change_status(self, pull_request, user, api=False):
152 reviewer = user.user_id in [x.user_id for x in
152 reviewer = user.user_id in [x.user_id for x in
153 pull_request.reviewers]
153 pull_request.reviewers]
154 return self.check_user_update(pull_request, user, api) or reviewer
154 return self.check_user_update(pull_request, user, api) or reviewer
155
155
156 def get(self, pull_request):
156 def get(self, pull_request):
157 return self.__get_pull_request(pull_request)
157 return self.__get_pull_request(pull_request)
158
158
159 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
159 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
160 opened_by=None, order_by=None,
160 opened_by=None, order_by=None,
161 order_dir='desc'):
161 order_dir='desc'):
162 repo = self._get_repo(repo_name)
162 repo = self._get_repo(repo_name)
163 q = PullRequest.query()
163 q = PullRequest.query()
164 # source or target
164 # source or target
165 if source:
165 if source:
166 q = q.filter(PullRequest.source_repo == repo)
166 q = q.filter(PullRequest.source_repo == repo)
167 else:
167 else:
168 q = q.filter(PullRequest.target_repo == repo)
168 q = q.filter(PullRequest.target_repo == repo)
169
169
170 # closed,opened
170 # closed,opened
171 if statuses:
171 if statuses:
172 q = q.filter(PullRequest.status.in_(statuses))
172 q = q.filter(PullRequest.status.in_(statuses))
173
173
174 # opened by filter
174 # opened by filter
175 if opened_by:
175 if opened_by:
176 q = q.filter(PullRequest.user_id.in_(opened_by))
176 q = q.filter(PullRequest.user_id.in_(opened_by))
177
177
178 if order_by:
178 if order_by:
179 order_map = {
179 order_map = {
180 'name_raw': PullRequest.pull_request_id,
180 'name_raw': PullRequest.pull_request_id,
181 'title': PullRequest.title,
181 'title': PullRequest.title,
182 'updated_on_raw': PullRequest.updated_on
182 'updated_on_raw': PullRequest.updated_on
183 }
183 }
184 if order_dir == 'asc':
184 if order_dir == 'asc':
185 q = q.order_by(order_map[order_by].asc())
185 q = q.order_by(order_map[order_by].asc())
186 else:
186 else:
187 q = q.order_by(order_map[order_by].desc())
187 q = q.order_by(order_map[order_by].desc())
188
188
189 return q
189 return q
190
190
191 def count_all(self, repo_name, source=False, statuses=None,
191 def count_all(self, repo_name, source=False, statuses=None,
192 opened_by=None):
192 opened_by=None):
193 """
193 """
194 Count the number of pull requests for a specific repository.
194 Count the number of pull requests for a specific repository.
195
195
196 :param repo_name: target or source repo
196 :param repo_name: target or source repo
197 :param source: boolean flag to specify if repo_name refers to source
197 :param source: boolean flag to specify if repo_name refers to source
198 :param statuses: list of pull request statuses
198 :param statuses: list of pull request statuses
199 :param opened_by: author user of the pull request
199 :param opened_by: author user of the pull request
200 :returns: int number of pull requests
200 :returns: int number of pull requests
201 """
201 """
202 q = self._prepare_get_all_query(
202 q = self._prepare_get_all_query(
203 repo_name, source=source, statuses=statuses, opened_by=opened_by)
203 repo_name, source=source, statuses=statuses, opened_by=opened_by)
204
204
205 return q.count()
205 return q.count()
206
206
207 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
207 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
208 offset=0, length=None, order_by=None, order_dir='desc'):
208 offset=0, length=None, order_by=None, order_dir='desc'):
209 """
209 """
210 Get all pull requests for a specific repository.
210 Get all pull requests for a specific repository.
211
211
212 :param repo_name: target or source repo
212 :param repo_name: target or source repo
213 :param source: boolean flag to specify if repo_name refers to source
213 :param source: boolean flag to specify if repo_name refers to source
214 :param statuses: list of pull request statuses
214 :param statuses: list of pull request statuses
215 :param opened_by: author user of the pull request
215 :param opened_by: author user of the pull request
216 :param offset: pagination offset
216 :param offset: pagination offset
217 :param length: length of returned list
217 :param length: length of returned list
218 :param order_by: order of the returned list
218 :param order_by: order of the returned list
219 :param order_dir: 'asc' or 'desc' ordering direction
219 :param order_dir: 'asc' or 'desc' ordering direction
220 :returns: list of pull requests
220 :returns: list of pull requests
221 """
221 """
222 q = self._prepare_get_all_query(
222 q = self._prepare_get_all_query(
223 repo_name, source=source, statuses=statuses, opened_by=opened_by,
223 repo_name, source=source, statuses=statuses, opened_by=opened_by,
224 order_by=order_by, order_dir=order_dir)
224 order_by=order_by, order_dir=order_dir)
225
225
226 if length:
226 if length:
227 pull_requests = q.limit(length).offset(offset).all()
227 pull_requests = q.limit(length).offset(offset).all()
228 else:
228 else:
229 pull_requests = q.all()
229 pull_requests = q.all()
230
230
231 return pull_requests
231 return pull_requests
232
232
233 def count_awaiting_review(self, repo_name, source=False, statuses=None,
233 def count_awaiting_review(self, repo_name, source=False, statuses=None,
234 opened_by=None):
234 opened_by=None):
235 """
235 """
236 Count the number of pull requests for a specific repository that are
236 Count the number of pull requests for a specific repository that are
237 awaiting review.
237 awaiting review.
238
238
239 :param repo_name: target or source repo
239 :param repo_name: target or source repo
240 :param source: boolean flag to specify if repo_name refers to source
240 :param source: boolean flag to specify if repo_name refers to source
241 :param statuses: list of pull request statuses
241 :param statuses: list of pull request statuses
242 :param opened_by: author user of the pull request
242 :param opened_by: author user of the pull request
243 :returns: int number of pull requests
243 :returns: int number of pull requests
244 """
244 """
245 pull_requests = self.get_awaiting_review(
245 pull_requests = self.get_awaiting_review(
246 repo_name, source=source, statuses=statuses, opened_by=opened_by)
246 repo_name, source=source, statuses=statuses, opened_by=opened_by)
247
247
248 return len(pull_requests)
248 return len(pull_requests)
249
249
250 def get_awaiting_review(self, repo_name, source=False, statuses=None,
250 def get_awaiting_review(self, repo_name, source=False, statuses=None,
251 opened_by=None, offset=0, length=None,
251 opened_by=None, offset=0, length=None,
252 order_by=None, order_dir='desc'):
252 order_by=None, order_dir='desc'):
253 """
253 """
254 Get all pull requests for a specific repository that are awaiting
254 Get all pull requests for a specific repository that are awaiting
255 review.
255 review.
256
256
257 :param repo_name: target or source repo
257 :param repo_name: target or source repo
258 :param source: boolean flag to specify if repo_name refers to source
258 :param source: boolean flag to specify if repo_name refers to source
259 :param statuses: list of pull request statuses
259 :param statuses: list of pull request statuses
260 :param opened_by: author user of the pull request
260 :param opened_by: author user of the pull request
261 :param offset: pagination offset
261 :param offset: pagination offset
262 :param length: length of returned list
262 :param length: length of returned list
263 :param order_by: order of the returned list
263 :param order_by: order of the returned list
264 :param order_dir: 'asc' or 'desc' ordering direction
264 :param order_dir: 'asc' or 'desc' ordering direction
265 :returns: list of pull requests
265 :returns: list of pull requests
266 """
266 """
267 pull_requests = self.get_all(
267 pull_requests = self.get_all(
268 repo_name, source=source, statuses=statuses, opened_by=opened_by,
268 repo_name, source=source, statuses=statuses, opened_by=opened_by,
269 order_by=order_by, order_dir=order_dir)
269 order_by=order_by, order_dir=order_dir)
270
270
271 _filtered_pull_requests = []
271 _filtered_pull_requests = []
272 for pr in pull_requests:
272 for pr in pull_requests:
273 status = pr.calculated_review_status()
273 status = pr.calculated_review_status()
274 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
274 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
275 ChangesetStatus.STATUS_UNDER_REVIEW]:
275 ChangesetStatus.STATUS_UNDER_REVIEW]:
276 _filtered_pull_requests.append(pr)
276 _filtered_pull_requests.append(pr)
277 if length:
277 if length:
278 return _filtered_pull_requests[offset:offset+length]
278 return _filtered_pull_requests[offset:offset+length]
279 else:
279 else:
280 return _filtered_pull_requests
280 return _filtered_pull_requests
281
281
282 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
282 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
283 opened_by=None, user_id=None):
283 opened_by=None, user_id=None):
284 """
284 """
285 Count the number of pull requests for a specific repository that are
285 Count the number of pull requests for a specific repository that are
286 awaiting review from a specific user.
286 awaiting review from a specific user.
287
287
288 :param repo_name: target or source repo
288 :param repo_name: target or source repo
289 :param source: boolean flag to specify if repo_name refers to source
289 :param source: boolean flag to specify if repo_name refers to source
290 :param statuses: list of pull request statuses
290 :param statuses: list of pull request statuses
291 :param opened_by: author user of the pull request
291 :param opened_by: author user of the pull request
292 :param user_id: reviewer user of the pull request
292 :param user_id: reviewer user of the pull request
293 :returns: int number of pull requests
293 :returns: int number of pull requests
294 """
294 """
295 pull_requests = self.get_awaiting_my_review(
295 pull_requests = self.get_awaiting_my_review(
296 repo_name, source=source, statuses=statuses, opened_by=opened_by,
296 repo_name, source=source, statuses=statuses, opened_by=opened_by,
297 user_id=user_id)
297 user_id=user_id)
298
298
299 return len(pull_requests)
299 return len(pull_requests)
300
300
301 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
301 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
302 opened_by=None, user_id=None, offset=0,
302 opened_by=None, user_id=None, offset=0,
303 length=None, order_by=None, order_dir='desc'):
303 length=None, order_by=None, order_dir='desc'):
304 """
304 """
305 Get all pull requests for a specific repository that are awaiting
305 Get all pull requests for a specific repository that are awaiting
306 review from a specific user.
306 review from a specific user.
307
307
308 :param repo_name: target or source repo
308 :param repo_name: target or source repo
309 :param source: boolean flag to specify if repo_name refers to source
309 :param source: boolean flag to specify if repo_name refers to source
310 :param statuses: list of pull request statuses
310 :param statuses: list of pull request statuses
311 :param opened_by: author user of the pull request
311 :param opened_by: author user of the pull request
312 :param user_id: reviewer user of the pull request
312 :param user_id: reviewer user of the pull request
313 :param offset: pagination offset
313 :param offset: pagination offset
314 :param length: length of returned list
314 :param length: length of returned list
315 :param order_by: order of the returned list
315 :param order_by: order of the returned list
316 :param order_dir: 'asc' or 'desc' ordering direction
316 :param order_dir: 'asc' or 'desc' ordering direction
317 :returns: list of pull requests
317 :returns: list of pull requests
318 """
318 """
319 pull_requests = self.get_all(
319 pull_requests = self.get_all(
320 repo_name, source=source, statuses=statuses, opened_by=opened_by,
320 repo_name, source=source, statuses=statuses, opened_by=opened_by,
321 order_by=order_by, order_dir=order_dir)
321 order_by=order_by, order_dir=order_dir)
322
322
323 _my = PullRequestModel().get_not_reviewed(user_id)
323 _my = PullRequestModel().get_not_reviewed(user_id)
324 my_participation = []
324 my_participation = []
325 for pr in pull_requests:
325 for pr in pull_requests:
326 if pr in _my:
326 if pr in _my:
327 my_participation.append(pr)
327 my_participation.append(pr)
328 _filtered_pull_requests = my_participation
328 _filtered_pull_requests = my_participation
329 if length:
329 if length:
330 return _filtered_pull_requests[offset:offset+length]
330 return _filtered_pull_requests[offset:offset+length]
331 else:
331 else:
332 return _filtered_pull_requests
332 return _filtered_pull_requests
333
333
334 def get_not_reviewed(self, user_id):
334 def get_not_reviewed(self, user_id):
335 return [
335 return [
336 x.pull_request for x in PullRequestReviewers.query().filter(
336 x.pull_request for x in PullRequestReviewers.query().filter(
337 PullRequestReviewers.user_id == user_id).all()
337 PullRequestReviewers.user_id == user_id).all()
338 ]
338 ]
339
339
340 def get_versions(self, pull_request):
340 def get_versions(self, pull_request):
341 """
341 """
342 returns version of pull request sorted by ID descending
342 returns version of pull request sorted by ID descending
343 """
343 """
344 return PullRequestVersion.query()\
344 return PullRequestVersion.query()\
345 .filter(PullRequestVersion.pull_request == pull_request)\
345 .filter(PullRequestVersion.pull_request == pull_request)\
346 .order_by(PullRequestVersion.pull_request_version_id.asc())\
346 .order_by(PullRequestVersion.pull_request_version_id.asc())\
347 .all()
347 .all()
348
348
349 def create(self, created_by, source_repo, source_ref, target_repo,
349 def create(self, created_by, source_repo, source_ref, target_repo,
350 target_ref, revisions, reviewers, title, description=None):
350 target_ref, revisions, reviewers, title, description=None):
351 created_by_user = self._get_user(created_by)
351 created_by_user = self._get_user(created_by)
352 source_repo = self._get_repo(source_repo)
352 source_repo = self._get_repo(source_repo)
353 target_repo = self._get_repo(target_repo)
353 target_repo = self._get_repo(target_repo)
354
354
355 pull_request = PullRequest()
355 pull_request = PullRequest()
356 pull_request.source_repo = source_repo
356 pull_request.source_repo = source_repo
357 pull_request.source_ref = source_ref
357 pull_request.source_ref = source_ref
358 pull_request.target_repo = target_repo
358 pull_request.target_repo = target_repo
359 pull_request.target_ref = target_ref
359 pull_request.target_ref = target_ref
360 pull_request.revisions = revisions
360 pull_request.revisions = revisions
361 pull_request.title = title
361 pull_request.title = title
362 pull_request.description = description
362 pull_request.description = description
363 pull_request.author = created_by_user
363 pull_request.author = created_by_user
364
364
365 Session().add(pull_request)
365 Session().add(pull_request)
366 Session().flush()
366 Session().flush()
367
367
368 reviewer_ids = set()
368 reviewer_ids = set()
369 # members / reviewers
369 # members / reviewers
370 for reviewer_object in reviewers:
370 for reviewer_object in reviewers:
371 if isinstance(reviewer_object, tuple):
371 if isinstance(reviewer_object, tuple):
372 user_id, reasons = reviewer_object
372 user_id, reasons = reviewer_object
373 else:
373 else:
374 user_id, reasons = reviewer_object, []
374 user_id, reasons = reviewer_object, []
375
375
376 user = self._get_user(user_id)
376 user = self._get_user(user_id)
377 reviewer_ids.add(user.user_id)
377 reviewer_ids.add(user.user_id)
378
378
379 reviewer = PullRequestReviewers(user, pull_request, reasons)
379 reviewer = PullRequestReviewers(user, pull_request, reasons)
380 Session().add(reviewer)
380 Session().add(reviewer)
381
381
382 # Set approval status to "Under Review" for all commits which are
382 # Set approval status to "Under Review" for all commits which are
383 # part of this pull request.
383 # part of this pull request.
384 ChangesetStatusModel().set_status(
384 ChangesetStatusModel().set_status(
385 repo=target_repo,
385 repo=target_repo,
386 status=ChangesetStatus.STATUS_UNDER_REVIEW,
386 status=ChangesetStatus.STATUS_UNDER_REVIEW,
387 user=created_by_user,
387 user=created_by_user,
388 pull_request=pull_request
388 pull_request=pull_request
389 )
389 )
390
390
391 self.notify_reviewers(pull_request, reviewer_ids)
391 self.notify_reviewers(pull_request, reviewer_ids)
392 self._trigger_pull_request_hook(
392 self._trigger_pull_request_hook(
393 pull_request, created_by_user, 'create')
393 pull_request, created_by_user, 'create')
394
394
395 return pull_request
395 return pull_request
396
396
397 def _trigger_pull_request_hook(self, pull_request, user, action):
397 def _trigger_pull_request_hook(self, pull_request, user, action):
398 pull_request = self.__get_pull_request(pull_request)
398 pull_request = self.__get_pull_request(pull_request)
399 target_scm = pull_request.target_repo.scm_instance()
399 target_scm = pull_request.target_repo.scm_instance()
400 if action == 'create':
400 if action == 'create':
401 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
401 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
402 elif action == 'merge':
402 elif action == 'merge':
403 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
403 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
404 elif action == 'close':
404 elif action == 'close':
405 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
405 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
406 elif action == 'review_status_change':
406 elif action == 'review_status_change':
407 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
407 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
408 elif action == 'update':
408 elif action == 'update':
409 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
409 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
410 else:
410 else:
411 return
411 return
412
412
413 trigger_hook(
413 trigger_hook(
414 username=user.username,
414 username=user.username,
415 repo_name=pull_request.target_repo.repo_name,
415 repo_name=pull_request.target_repo.repo_name,
416 repo_alias=target_scm.alias,
416 repo_alias=target_scm.alias,
417 pull_request=pull_request)
417 pull_request=pull_request)
418
418
419 def _get_commit_ids(self, pull_request):
419 def _get_commit_ids(self, pull_request):
420 """
420 """
421 Return the commit ids of the merged pull request.
421 Return the commit ids of the merged pull request.
422
422
423 This method is not dealing correctly yet with the lack of autoupdates
423 This method is not dealing correctly yet with the lack of autoupdates
424 nor with the implicit target updates.
424 nor with the implicit target updates.
425 For example: if a commit in the source repo is already in the target it
425 For example: if a commit in the source repo is already in the target it
426 will be reported anyways.
426 will be reported anyways.
427 """
427 """
428 merge_rev = pull_request.merge_rev
428 merge_rev = pull_request.merge_rev
429 if merge_rev is None:
429 if merge_rev is None:
430 raise ValueError('This pull request was not merged yet')
430 raise ValueError('This pull request was not merged yet')
431
431
432 commit_ids = list(pull_request.revisions)
432 commit_ids = list(pull_request.revisions)
433 if merge_rev not in commit_ids:
433 if merge_rev not in commit_ids:
434 commit_ids.append(merge_rev)
434 commit_ids.append(merge_rev)
435
435
436 return commit_ids
436 return commit_ids
437
437
438 def merge(self, pull_request, user, extras):
438 def merge(self, pull_request, user, extras):
439 log.debug("Merging pull request %s", pull_request.pull_request_id)
439 log.debug("Merging pull request %s", pull_request.pull_request_id)
440 merge_state = self._merge_pull_request(pull_request, user, extras)
440 merge_state = self._merge_pull_request(pull_request, user, extras)
441 if merge_state.executed:
441 if merge_state.executed:
442 log.debug(
442 log.debug(
443 "Merge was successful, updating the pull request comments.")
443 "Merge was successful, updating the pull request comments.")
444 self._comment_and_close_pr(pull_request, user, merge_state)
444 self._comment_and_close_pr(pull_request, user, merge_state)
445 self._log_action('user_merged_pull_request', user, pull_request)
445 self._log_action('user_merged_pull_request', user, pull_request)
446 else:
446 else:
447 log.warn("Merge failed, not updating the pull request.")
447 log.warn("Merge failed, not updating the pull request.")
448 return merge_state
448 return merge_state
449
449
450 def _merge_pull_request(self, pull_request, user, extras):
450 def _merge_pull_request(self, pull_request, user, extras):
451 target_vcs = pull_request.target_repo.scm_instance()
451 target_vcs = pull_request.target_repo.scm_instance()
452 source_vcs = pull_request.source_repo.scm_instance()
452 source_vcs = pull_request.source_repo.scm_instance()
453 target_ref = self._refresh_reference(
453 target_ref = self._refresh_reference(
454 pull_request.target_ref_parts, target_vcs)
454 pull_request.target_ref_parts, target_vcs)
455
455
456 message = _(
456 message = _(
457 'Merge pull request #%(pr_id)s from '
457 'Merge pull request #%(pr_id)s from '
458 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
458 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
459 'pr_id': pull_request.pull_request_id,
459 'pr_id': pull_request.pull_request_id,
460 'source_repo': source_vcs.name,
460 'source_repo': source_vcs.name,
461 'source_ref_name': pull_request.source_ref_parts.name,
461 'source_ref_name': pull_request.source_ref_parts.name,
462 'pr_title': pull_request.title
462 'pr_title': pull_request.title
463 }
463 }
464
464
465 workspace_id = self._workspace_id(pull_request)
465 workspace_id = self._workspace_id(pull_request)
466 use_rebase = self._use_rebase_for_merging(pull_request)
466 use_rebase = self._use_rebase_for_merging(pull_request)
467
467
468 callback_daemon, extras = prepare_callback_daemon(
468 callback_daemon, extras = prepare_callback_daemon(
469 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
469 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
470 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
470 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
471
471
472 with callback_daemon:
472 with callback_daemon:
473 # TODO: johbo: Implement a clean way to run a config_override
473 # TODO: johbo: Implement a clean way to run a config_override
474 # for a single call.
474 # for a single call.
475 target_vcs.config.set(
475 target_vcs.config.set(
476 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
476 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
477 merge_state = target_vcs.merge(
477 merge_state = target_vcs.merge(
478 target_ref, source_vcs, pull_request.source_ref_parts,
478 target_ref, source_vcs, pull_request.source_ref_parts,
479 workspace_id, user_name=user.username,
479 workspace_id, user_name=user.username,
480 user_email=user.email, message=message, use_rebase=use_rebase)
480 user_email=user.email, message=message, use_rebase=use_rebase)
481 return merge_state
481 return merge_state
482
482
483 def _comment_and_close_pr(self, pull_request, user, merge_state):
483 def _comment_and_close_pr(self, pull_request, user, merge_state):
484 pull_request.merge_rev = merge_state.merge_ref.commit_id
484 pull_request.merge_rev = merge_state.merge_ref.commit_id
485 pull_request.updated_on = datetime.datetime.now()
485 pull_request.updated_on = datetime.datetime.now()
486
486
487 ChangesetCommentsModel().create(
487 ChangesetCommentsModel().create(
488 text=unicode(_('Pull request merged and closed')),
488 text=unicode(_('Pull request merged and closed')),
489 repo=pull_request.target_repo.repo_id,
489 repo=pull_request.target_repo.repo_id,
490 user=user.user_id,
490 user=user.user_id,
491 pull_request=pull_request.pull_request_id,
491 pull_request=pull_request.pull_request_id,
492 f_path=None,
492 f_path=None,
493 line_no=None,
493 line_no=None,
494 closing_pr=True
494 closing_pr=True
495 )
495 )
496
496
497 Session().add(pull_request)
497 Session().add(pull_request)
498 Session().flush()
498 Session().flush()
499 # TODO: paris: replace invalidation with less radical solution
499 # TODO: paris: replace invalidation with less radical solution
500 ScmModel().mark_for_invalidation(
500 ScmModel().mark_for_invalidation(
501 pull_request.target_repo.repo_name)
501 pull_request.target_repo.repo_name)
502 self._trigger_pull_request_hook(pull_request, user, 'merge')
502 self._trigger_pull_request_hook(pull_request, user, 'merge')
503
503
504 def has_valid_update_type(self, pull_request):
504 def has_valid_update_type(self, pull_request):
505 source_ref_type = pull_request.source_ref_parts.type
505 source_ref_type = pull_request.source_ref_parts.type
506 return source_ref_type in ['book', 'branch', 'tag']
506 return source_ref_type in ['book', 'branch', 'tag']
507
507
508 def update_commits(self, pull_request):
508 def update_commits(self, pull_request):
509 """
509 """
510 Get the updated list of commits for the pull request
510 Get the updated list of commits for the pull request
511 and return the new pull request version and the list
511 and return the new pull request version and the list
512 of commits processed by this update action
512 of commits processed by this update action
513 """
513 """
514 pull_request = self.__get_pull_request(pull_request)
514 pull_request = self.__get_pull_request(pull_request)
515 source_ref_type = pull_request.source_ref_parts.type
515 source_ref_type = pull_request.source_ref_parts.type
516 source_ref_name = pull_request.source_ref_parts.name
516 source_ref_name = pull_request.source_ref_parts.name
517 source_ref_id = pull_request.source_ref_parts.commit_id
517 source_ref_id = pull_request.source_ref_parts.commit_id
518
518
519 if not self.has_valid_update_type(pull_request):
519 if not self.has_valid_update_type(pull_request):
520 log.debug(
520 log.debug(
521 "Skipping update of pull request %s due to ref type: %s",
521 "Skipping update of pull request %s due to ref type: %s",
522 pull_request, source_ref_type)
522 pull_request, source_ref_type)
523 return UpdateResponse(
523 return UpdateResponse(
524 success=False,
524 success=False,
525 reason=UpdateFailureReason.WRONG_REF_TPYE,
525 reason=UpdateFailureReason.WRONG_REF_TPYE,
526 old=pull_request, new=None, changes=None)
526 old=pull_request, new=None, changes=None)
527
527
528 source_repo = pull_request.source_repo.scm_instance()
528 source_repo = pull_request.source_repo.scm_instance()
529 try:
529 try:
530 source_commit = source_repo.get_commit(commit_id=source_ref_name)
530 source_commit = source_repo.get_commit(commit_id=source_ref_name)
531 except CommitDoesNotExistError:
531 except CommitDoesNotExistError:
532 return UpdateResponse(
532 return UpdateResponse(
533 success=False,
533 success=False,
534 reason=UpdateFailureReason.MISSING_SOURCE_REF,
534 reason=UpdateFailureReason.MISSING_SOURCE_REF,
535 old=pull_request, new=None, changes=None)
535 old=pull_request, new=None, changes=None)
536
536
537 if source_ref_id == source_commit.raw_id:
537 if source_ref_id == source_commit.raw_id:
538 log.debug("Nothing changed in pull request %s", pull_request)
538 log.debug("Nothing changed in pull request %s", pull_request)
539 return UpdateResponse(
539 return UpdateResponse(
540 success=True,
540 success=False,
541 reason=UpdateFailureReason.NO_CHANGE,
541 reason=UpdateFailureReason.NO_CHANGE,
542 old=pull_request, new=None, changes=None)
542 old=pull_request, new=None, changes=None)
543
543
544 # Finally there is a need for an update
544 # Finally there is a need for an update
545 pull_request_version = self._create_version_from_snapshot(pull_request)
545 pull_request_version = self._create_version_from_snapshot(pull_request)
546 self._link_comments_to_version(pull_request_version)
546 self._link_comments_to_version(pull_request_version)
547
547
548 target_ref_type = pull_request.target_ref_parts.type
548 target_ref_type = pull_request.target_ref_parts.type
549 target_ref_name = pull_request.target_ref_parts.name
549 target_ref_name = pull_request.target_ref_parts.name
550 target_ref_id = pull_request.target_ref_parts.commit_id
550 target_ref_id = pull_request.target_ref_parts.commit_id
551 target_repo = pull_request.target_repo.scm_instance()
551 target_repo = pull_request.target_repo.scm_instance()
552
552
553 try:
553 try:
554 if target_ref_type in ('tag', 'branch', 'book'):
554 if target_ref_type in ('tag', 'branch', 'book'):
555 target_commit = target_repo.get_commit(target_ref_name)
555 target_commit = target_repo.get_commit(target_ref_name)
556 else:
556 else:
557 target_commit = target_repo.get_commit(target_ref_id)
557 target_commit = target_repo.get_commit(target_ref_id)
558 except CommitDoesNotExistError:
558 except CommitDoesNotExistError:
559 return UpdateResponse(
559 return UpdateResponse(
560 success=False,
560 success=False,
561 reason=UpdateFailureReason.MISSING_TARGET_REF,
561 reason=UpdateFailureReason.MISSING_TARGET_REF,
562 old=pull_request, new=None, changes=None)
562 old=pull_request, new=None, changes=None)
563
563
564 # re-compute commit ids
564 # re-compute commit ids
565 old_commit_ids = set(pull_request.revisions)
565 old_commit_ids = set(pull_request.revisions)
566 pre_load = ["author", "branch", "date", "message"]
566 pre_load = ["author", "branch", "date", "message"]
567 commit_ranges = target_repo.compare(
567 commit_ranges = target_repo.compare(
568 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
568 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
569 pre_load=pre_load)
569 pre_load=pre_load)
570
570
571 ancestor = target_repo.get_common_ancestor(
571 ancestor = target_repo.get_common_ancestor(
572 target_commit.raw_id, source_commit.raw_id, source_repo)
572 target_commit.raw_id, source_commit.raw_id, source_repo)
573
573
574 pull_request.source_ref = '%s:%s:%s' % (
574 pull_request.source_ref = '%s:%s:%s' % (
575 source_ref_type, source_ref_name, source_commit.raw_id)
575 source_ref_type, source_ref_name, source_commit.raw_id)
576 pull_request.target_ref = '%s:%s:%s' % (
576 pull_request.target_ref = '%s:%s:%s' % (
577 target_ref_type, target_ref_name, ancestor)
577 target_ref_type, target_ref_name, ancestor)
578 pull_request.revisions = [
578 pull_request.revisions = [
579 commit.raw_id for commit in reversed(commit_ranges)]
579 commit.raw_id for commit in reversed(commit_ranges)]
580 pull_request.updated_on = datetime.datetime.now()
580 pull_request.updated_on = datetime.datetime.now()
581 Session().add(pull_request)
581 Session().add(pull_request)
582 new_commit_ids = set(pull_request.revisions)
582 new_commit_ids = set(pull_request.revisions)
583
583
584 changes = self._calculate_commit_id_changes(
584 changes = self._calculate_commit_id_changes(
585 old_commit_ids, new_commit_ids)
585 old_commit_ids, new_commit_ids)
586
586
587 old_diff_data, new_diff_data = self._generate_update_diffs(
587 old_diff_data, new_diff_data = self._generate_update_diffs(
588 pull_request, pull_request_version)
588 pull_request, pull_request_version)
589
589
590 ChangesetCommentsModel().outdate_comments(
590 ChangesetCommentsModel().outdate_comments(
591 pull_request, old_diff_data=old_diff_data,
591 pull_request, old_diff_data=old_diff_data,
592 new_diff_data=new_diff_data)
592 new_diff_data=new_diff_data)
593
593
594 file_changes = self._calculate_file_changes(
594 file_changes = self._calculate_file_changes(
595 old_diff_data, new_diff_data)
595 old_diff_data, new_diff_data)
596
596
597 # Add an automatic comment to the pull request
597 # Add an automatic comment to the pull request
598 update_comment = ChangesetCommentsModel().create(
598 update_comment = ChangesetCommentsModel().create(
599 text=self._render_update_message(changes, file_changes),
599 text=self._render_update_message(changes, file_changes),
600 repo=pull_request.target_repo,
600 repo=pull_request.target_repo,
601 user=pull_request.author,
601 user=pull_request.author,
602 pull_request=pull_request,
602 pull_request=pull_request,
603 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
603 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
604
604
605 # Update status to "Under Review" for added commits
605 # Update status to "Under Review" for added commits
606 for commit_id in changes.added:
606 for commit_id in changes.added:
607 ChangesetStatusModel().set_status(
607 ChangesetStatusModel().set_status(
608 repo=pull_request.source_repo,
608 repo=pull_request.source_repo,
609 status=ChangesetStatus.STATUS_UNDER_REVIEW,
609 status=ChangesetStatus.STATUS_UNDER_REVIEW,
610 comment=update_comment,
610 comment=update_comment,
611 user=pull_request.author,
611 user=pull_request.author,
612 pull_request=pull_request,
612 pull_request=pull_request,
613 revision=commit_id)
613 revision=commit_id)
614
614
615 log.debug(
615 log.debug(
616 'Updated pull request %s, added_ids: %s, common_ids: %s, '
616 'Updated pull request %s, added_ids: %s, common_ids: %s, '
617 'removed_ids: %s', pull_request.pull_request_id,
617 'removed_ids: %s', pull_request.pull_request_id,
618 changes.added, changes.common, changes.removed)
618 changes.added, changes.common, changes.removed)
619 log.debug('Updated pull request with the following file changes: %s',
619 log.debug('Updated pull request with the following file changes: %s',
620 file_changes)
620 file_changes)
621
621
622 log.info(
622 log.info(
623 "Updated pull request %s from commit %s to commit %s, "
623 "Updated pull request %s from commit %s to commit %s, "
624 "stored new version %s of this pull request.",
624 "stored new version %s of this pull request.",
625 pull_request.pull_request_id, source_ref_id,
625 pull_request.pull_request_id, source_ref_id,
626 pull_request.source_ref_parts.commit_id,
626 pull_request.source_ref_parts.commit_id,
627 pull_request_version.pull_request_version_id)
627 pull_request_version.pull_request_version_id)
628 Session().commit()
628 Session().commit()
629 self._trigger_pull_request_hook(pull_request, pull_request.author,
629 self._trigger_pull_request_hook(pull_request, pull_request.author,
630 'update')
630 'update')
631
631
632 return UpdateResponse(
632 return UpdateResponse(
633 success=True, reason=UpdateFailureReason.NONE,
633 success=True, reason=UpdateFailureReason.NONE,
634 old=pull_request, new=pull_request_version, changes=changes)
634 old=pull_request, new=pull_request_version, changes=changes)
635
635
636 def _create_version_from_snapshot(self, pull_request):
636 def _create_version_from_snapshot(self, pull_request):
637 version = PullRequestVersion()
637 version = PullRequestVersion()
638 version.title = pull_request.title
638 version.title = pull_request.title
639 version.description = pull_request.description
639 version.description = pull_request.description
640 version.status = pull_request.status
640 version.status = pull_request.status
641 version.created_on = pull_request.created_on
641 version.created_on = pull_request.created_on
642 version.updated_on = pull_request.updated_on
642 version.updated_on = pull_request.updated_on
643 version.user_id = pull_request.user_id
643 version.user_id = pull_request.user_id
644 version.source_repo = pull_request.source_repo
644 version.source_repo = pull_request.source_repo
645 version.source_ref = pull_request.source_ref
645 version.source_ref = pull_request.source_ref
646 version.target_repo = pull_request.target_repo
646 version.target_repo = pull_request.target_repo
647 version.target_ref = pull_request.target_ref
647 version.target_ref = pull_request.target_ref
648
648
649 version._last_merge_source_rev = pull_request._last_merge_source_rev
649 version._last_merge_source_rev = pull_request._last_merge_source_rev
650 version._last_merge_target_rev = pull_request._last_merge_target_rev
650 version._last_merge_target_rev = pull_request._last_merge_target_rev
651 version._last_merge_status = pull_request._last_merge_status
651 version._last_merge_status = pull_request._last_merge_status
652 version.shadow_merge_ref = pull_request.shadow_merge_ref
652 version.shadow_merge_ref = pull_request.shadow_merge_ref
653 version.merge_rev = pull_request.merge_rev
653 version.merge_rev = pull_request.merge_rev
654
654
655 version.revisions = pull_request.revisions
655 version.revisions = pull_request.revisions
656 version.pull_request = pull_request
656 version.pull_request = pull_request
657 Session().add(version)
657 Session().add(version)
658 Session().flush()
658 Session().flush()
659
659
660 return version
660 return version
661
661
662 def _generate_update_diffs(self, pull_request, pull_request_version):
662 def _generate_update_diffs(self, pull_request, pull_request_version):
663 diff_context = (
663 diff_context = (
664 self.DIFF_CONTEXT +
664 self.DIFF_CONTEXT +
665 ChangesetCommentsModel.needed_extra_diff_context())
665 ChangesetCommentsModel.needed_extra_diff_context())
666 old_diff = self._get_diff_from_pr_or_version(
666 old_diff = self._get_diff_from_pr_or_version(
667 pull_request_version, context=diff_context)
667 pull_request_version, context=diff_context)
668 new_diff = self._get_diff_from_pr_or_version(
668 new_diff = self._get_diff_from_pr_or_version(
669 pull_request, context=diff_context)
669 pull_request, context=diff_context)
670
670
671 old_diff_data = diffs.DiffProcessor(old_diff)
671 old_diff_data = diffs.DiffProcessor(old_diff)
672 old_diff_data.prepare()
672 old_diff_data.prepare()
673 new_diff_data = diffs.DiffProcessor(new_diff)
673 new_diff_data = diffs.DiffProcessor(new_diff)
674 new_diff_data.prepare()
674 new_diff_data.prepare()
675
675
676 return old_diff_data, new_diff_data
676 return old_diff_data, new_diff_data
677
677
678 def _link_comments_to_version(self, pull_request_version):
678 def _link_comments_to_version(self, pull_request_version):
679 """
679 """
680 Link all unlinked comments of this pull request to the given version.
680 Link all unlinked comments of this pull request to the given version.
681
681
682 :param pull_request_version: The `PullRequestVersion` to which
682 :param pull_request_version: The `PullRequestVersion` to which
683 the comments shall be linked.
683 the comments shall be linked.
684
684
685 """
685 """
686 pull_request = pull_request_version.pull_request
686 pull_request = pull_request_version.pull_request
687 comments = ChangesetComment.query().filter(
687 comments = ChangesetComment.query().filter(
688 # TODO: johbo: Should we query for the repo at all here?
688 # TODO: johbo: Should we query for the repo at all here?
689 # Pending decision on how comments of PRs are to be related
689 # Pending decision on how comments of PRs are to be related
690 # to either the source repo, the target repo or no repo at all.
690 # to either the source repo, the target repo or no repo at all.
691 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
691 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
692 ChangesetComment.pull_request == pull_request,
692 ChangesetComment.pull_request == pull_request,
693 ChangesetComment.pull_request_version == None)
693 ChangesetComment.pull_request_version == None)
694
694
695 # TODO: johbo: Find out why this breaks if it is done in a bulk
695 # TODO: johbo: Find out why this breaks if it is done in a bulk
696 # operation.
696 # operation.
697 for comment in comments:
697 for comment in comments:
698 comment.pull_request_version_id = (
698 comment.pull_request_version_id = (
699 pull_request_version.pull_request_version_id)
699 pull_request_version.pull_request_version_id)
700 Session().add(comment)
700 Session().add(comment)
701
701
702 def _calculate_commit_id_changes(self, old_ids, new_ids):
702 def _calculate_commit_id_changes(self, old_ids, new_ids):
703 added = new_ids.difference(old_ids)
703 added = new_ids.difference(old_ids)
704 common = old_ids.intersection(new_ids)
704 common = old_ids.intersection(new_ids)
705 removed = old_ids.difference(new_ids)
705 removed = old_ids.difference(new_ids)
706 return ChangeTuple(added, common, removed)
706 return ChangeTuple(added, common, removed)
707
707
708 def _calculate_file_changes(self, old_diff_data, new_diff_data):
708 def _calculate_file_changes(self, old_diff_data, new_diff_data):
709
709
710 old_files = OrderedDict()
710 old_files = OrderedDict()
711 for diff_data in old_diff_data.parsed_diff:
711 for diff_data in old_diff_data.parsed_diff:
712 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
712 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
713
713
714 added_files = []
714 added_files = []
715 modified_files = []
715 modified_files = []
716 removed_files = []
716 removed_files = []
717 for diff_data in new_diff_data.parsed_diff:
717 for diff_data in new_diff_data.parsed_diff:
718 new_filename = diff_data['filename']
718 new_filename = diff_data['filename']
719 new_hash = md5_safe(diff_data['raw_diff'])
719 new_hash = md5_safe(diff_data['raw_diff'])
720
720
721 old_hash = old_files.get(new_filename)
721 old_hash = old_files.get(new_filename)
722 if not old_hash:
722 if not old_hash:
723 # file is not present in old diff, means it's added
723 # file is not present in old diff, means it's added
724 added_files.append(new_filename)
724 added_files.append(new_filename)
725 else:
725 else:
726 if new_hash != old_hash:
726 if new_hash != old_hash:
727 modified_files.append(new_filename)
727 modified_files.append(new_filename)
728 # now remove a file from old, since we have seen it already
728 # now remove a file from old, since we have seen it already
729 del old_files[new_filename]
729 del old_files[new_filename]
730
730
731 # removed files is when there are present in old, but not in NEW,
731 # removed files is when there are present in old, but not in NEW,
732 # since we remove old files that are present in new diff, left-overs
732 # since we remove old files that are present in new diff, left-overs
733 # if any should be the removed files
733 # if any should be the removed files
734 removed_files.extend(old_files.keys())
734 removed_files.extend(old_files.keys())
735
735
736 return FileChangeTuple(added_files, modified_files, removed_files)
736 return FileChangeTuple(added_files, modified_files, removed_files)
737
737
738 def _render_update_message(self, changes, file_changes):
738 def _render_update_message(self, changes, file_changes):
739 """
739 """
740 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
740 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
741 so it's always looking the same disregarding on which default
741 so it's always looking the same disregarding on which default
742 renderer system is using.
742 renderer system is using.
743
743
744 :param changes: changes named tuple
744 :param changes: changes named tuple
745 :param file_changes: file changes named tuple
745 :param file_changes: file changes named tuple
746
746
747 """
747 """
748 new_status = ChangesetStatus.get_status_lbl(
748 new_status = ChangesetStatus.get_status_lbl(
749 ChangesetStatus.STATUS_UNDER_REVIEW)
749 ChangesetStatus.STATUS_UNDER_REVIEW)
750
750
751 changed_files = (
751 changed_files = (
752 file_changes.added + file_changes.modified + file_changes.removed)
752 file_changes.added + file_changes.modified + file_changes.removed)
753
753
754 params = {
754 params = {
755 'under_review_label': new_status,
755 'under_review_label': new_status,
756 'added_commits': changes.added,
756 'added_commits': changes.added,
757 'removed_commits': changes.removed,
757 'removed_commits': changes.removed,
758 'changed_files': changed_files,
758 'changed_files': changed_files,
759 'added_files': file_changes.added,
759 'added_files': file_changes.added,
760 'modified_files': file_changes.modified,
760 'modified_files': file_changes.modified,
761 'removed_files': file_changes.removed,
761 'removed_files': file_changes.removed,
762 }
762 }
763 renderer = RstTemplateRenderer()
763 renderer = RstTemplateRenderer()
764 return renderer.render('pull_request_update.mako', **params)
764 return renderer.render('pull_request_update.mako', **params)
765
765
766 def edit(self, pull_request, title, description):
766 def edit(self, pull_request, title, description):
767 pull_request = self.__get_pull_request(pull_request)
767 pull_request = self.__get_pull_request(pull_request)
768 if pull_request.is_closed():
768 if pull_request.is_closed():
769 raise ValueError('This pull request is closed')
769 raise ValueError('This pull request is closed')
770 if title:
770 if title:
771 pull_request.title = title
771 pull_request.title = title
772 pull_request.description = description
772 pull_request.description = description
773 pull_request.updated_on = datetime.datetime.now()
773 pull_request.updated_on = datetime.datetime.now()
774 Session().add(pull_request)
774 Session().add(pull_request)
775
775
776 def update_reviewers(self, pull_request, reviewer_data):
776 def update_reviewers(self, pull_request, reviewer_data):
777 """
777 """
778 Update the reviewers in the pull request
778 Update the reviewers in the pull request
779
779
780 :param pull_request: the pr to update
780 :param pull_request: the pr to update
781 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
781 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
782 """
782 """
783
783
784 reviewers_reasons = {}
784 reviewers_reasons = {}
785 for user_id, reasons in reviewer_data:
785 for user_id, reasons in reviewer_data:
786 if isinstance(user_id, (int, basestring)):
786 if isinstance(user_id, (int, basestring)):
787 user_id = self._get_user(user_id).user_id
787 user_id = self._get_user(user_id).user_id
788 reviewers_reasons[user_id] = reasons
788 reviewers_reasons[user_id] = reasons
789
789
790 reviewers_ids = set(reviewers_reasons.keys())
790 reviewers_ids = set(reviewers_reasons.keys())
791 pull_request = self.__get_pull_request(pull_request)
791 pull_request = self.__get_pull_request(pull_request)
792 current_reviewers = PullRequestReviewers.query()\
792 current_reviewers = PullRequestReviewers.query()\
793 .filter(PullRequestReviewers.pull_request ==
793 .filter(PullRequestReviewers.pull_request ==
794 pull_request).all()
794 pull_request).all()
795 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
795 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
796
796
797 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
797 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
798 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
798 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
799
799
800 log.debug("Adding %s reviewers", ids_to_add)
800 log.debug("Adding %s reviewers", ids_to_add)
801 log.debug("Removing %s reviewers", ids_to_remove)
801 log.debug("Removing %s reviewers", ids_to_remove)
802 changed = False
802 changed = False
803 for uid in ids_to_add:
803 for uid in ids_to_add:
804 changed = True
804 changed = True
805 _usr = self._get_user(uid)
805 _usr = self._get_user(uid)
806 reasons = reviewers_reasons[uid]
806 reasons = reviewers_reasons[uid]
807 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
807 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
808 Session().add(reviewer)
808 Session().add(reviewer)
809
809
810 self.notify_reviewers(pull_request, ids_to_add)
810 self.notify_reviewers(pull_request, ids_to_add)
811
811
812 for uid in ids_to_remove:
812 for uid in ids_to_remove:
813 changed = True
813 changed = True
814 reviewer = PullRequestReviewers.query()\
814 reviewer = PullRequestReviewers.query()\
815 .filter(PullRequestReviewers.user_id == uid,
815 .filter(PullRequestReviewers.user_id == uid,
816 PullRequestReviewers.pull_request == pull_request)\
816 PullRequestReviewers.pull_request == pull_request)\
817 .scalar()
817 .scalar()
818 if reviewer:
818 if reviewer:
819 Session().delete(reviewer)
819 Session().delete(reviewer)
820 if changed:
820 if changed:
821 pull_request.updated_on = datetime.datetime.now()
821 pull_request.updated_on = datetime.datetime.now()
822 Session().add(pull_request)
822 Session().add(pull_request)
823
823
824 return ids_to_add, ids_to_remove
824 return ids_to_add, ids_to_remove
825
825
826 def get_url(self, pull_request):
826 def get_url(self, pull_request):
827 return h.url('pullrequest_show',
827 return h.url('pullrequest_show',
828 repo_name=safe_str(pull_request.target_repo.repo_name),
828 repo_name=safe_str(pull_request.target_repo.repo_name),
829 pull_request_id=pull_request.pull_request_id,
829 pull_request_id=pull_request.pull_request_id,
830 qualified=True)
830 qualified=True)
831
831
832 def get_shadow_clone_url(self, pull_request):
832 def get_shadow_clone_url(self, pull_request):
833 """
833 """
834 Returns qualified url pointing to the shadow repository. If this pull
834 Returns qualified url pointing to the shadow repository. If this pull
835 request is closed there is no shadow repository and ``None`` will be
835 request is closed there is no shadow repository and ``None`` will be
836 returned.
836 returned.
837 """
837 """
838 if pull_request.is_closed():
838 if pull_request.is_closed():
839 return None
839 return None
840 else:
840 else:
841 pr_url = urllib.unquote(self.get_url(pull_request))
841 pr_url = urllib.unquote(self.get_url(pull_request))
842 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
842 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
843
843
844 def notify_reviewers(self, pull_request, reviewers_ids):
844 def notify_reviewers(self, pull_request, reviewers_ids):
845 # notification to reviewers
845 # notification to reviewers
846 if not reviewers_ids:
846 if not reviewers_ids:
847 return
847 return
848
848
849 pull_request_obj = pull_request
849 pull_request_obj = pull_request
850 # get the current participants of this pull request
850 # get the current participants of this pull request
851 recipients = reviewers_ids
851 recipients = reviewers_ids
852 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
852 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
853
853
854 pr_source_repo = pull_request_obj.source_repo
854 pr_source_repo = pull_request_obj.source_repo
855 pr_target_repo = pull_request_obj.target_repo
855 pr_target_repo = pull_request_obj.target_repo
856
856
857 pr_url = h.url(
857 pr_url = h.url(
858 'pullrequest_show',
858 'pullrequest_show',
859 repo_name=pr_target_repo.repo_name,
859 repo_name=pr_target_repo.repo_name,
860 pull_request_id=pull_request_obj.pull_request_id,
860 pull_request_id=pull_request_obj.pull_request_id,
861 qualified=True,)
861 qualified=True,)
862
862
863 # set some variables for email notification
863 # set some variables for email notification
864 pr_target_repo_url = h.url(
864 pr_target_repo_url = h.url(
865 'summary_home',
865 'summary_home',
866 repo_name=pr_target_repo.repo_name,
866 repo_name=pr_target_repo.repo_name,
867 qualified=True)
867 qualified=True)
868
868
869 pr_source_repo_url = h.url(
869 pr_source_repo_url = h.url(
870 'summary_home',
870 'summary_home',
871 repo_name=pr_source_repo.repo_name,
871 repo_name=pr_source_repo.repo_name,
872 qualified=True)
872 qualified=True)
873
873
874 # pull request specifics
874 # pull request specifics
875 pull_request_commits = [
875 pull_request_commits = [
876 (x.raw_id, x.message)
876 (x.raw_id, x.message)
877 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
877 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
878
878
879 kwargs = {
879 kwargs = {
880 'user': pull_request.author,
880 'user': pull_request.author,
881 'pull_request': pull_request_obj,
881 'pull_request': pull_request_obj,
882 'pull_request_commits': pull_request_commits,
882 'pull_request_commits': pull_request_commits,
883
883
884 'pull_request_target_repo': pr_target_repo,
884 'pull_request_target_repo': pr_target_repo,
885 'pull_request_target_repo_url': pr_target_repo_url,
885 'pull_request_target_repo_url': pr_target_repo_url,
886
886
887 'pull_request_source_repo': pr_source_repo,
887 'pull_request_source_repo': pr_source_repo,
888 'pull_request_source_repo_url': pr_source_repo_url,
888 'pull_request_source_repo_url': pr_source_repo_url,
889
889
890 'pull_request_url': pr_url,
890 'pull_request_url': pr_url,
891 }
891 }
892
892
893 # pre-generate the subject for notification itself
893 # pre-generate the subject for notification itself
894 (subject,
894 (subject,
895 _h, _e, # we don't care about those
895 _h, _e, # we don't care about those
896 body_plaintext) = EmailNotificationModel().render_email(
896 body_plaintext) = EmailNotificationModel().render_email(
897 notification_type, **kwargs)
897 notification_type, **kwargs)
898
898
899 # create notification objects, and emails
899 # create notification objects, and emails
900 NotificationModel().create(
900 NotificationModel().create(
901 created_by=pull_request.author,
901 created_by=pull_request.author,
902 notification_subject=subject,
902 notification_subject=subject,
903 notification_body=body_plaintext,
903 notification_body=body_plaintext,
904 notification_type=notification_type,
904 notification_type=notification_type,
905 recipients=recipients,
905 recipients=recipients,
906 email_kwargs=kwargs,
906 email_kwargs=kwargs,
907 )
907 )
908
908
909 def delete(self, pull_request):
909 def delete(self, pull_request):
910 pull_request = self.__get_pull_request(pull_request)
910 pull_request = self.__get_pull_request(pull_request)
911 self._cleanup_merge_workspace(pull_request)
911 self._cleanup_merge_workspace(pull_request)
912 Session().delete(pull_request)
912 Session().delete(pull_request)
913
913
914 def close_pull_request(self, pull_request, user):
914 def close_pull_request(self, pull_request, user):
915 pull_request = self.__get_pull_request(pull_request)
915 pull_request = self.__get_pull_request(pull_request)
916 self._cleanup_merge_workspace(pull_request)
916 self._cleanup_merge_workspace(pull_request)
917 pull_request.status = PullRequest.STATUS_CLOSED
917 pull_request.status = PullRequest.STATUS_CLOSED
918 pull_request.updated_on = datetime.datetime.now()
918 pull_request.updated_on = datetime.datetime.now()
919 Session().add(pull_request)
919 Session().add(pull_request)
920 self._trigger_pull_request_hook(
920 self._trigger_pull_request_hook(
921 pull_request, pull_request.author, 'close')
921 pull_request, pull_request.author, 'close')
922 self._log_action('user_closed_pull_request', user, pull_request)
922 self._log_action('user_closed_pull_request', user, pull_request)
923
923
924 def close_pull_request_with_comment(self, pull_request, user, repo,
924 def close_pull_request_with_comment(self, pull_request, user, repo,
925 message=None):
925 message=None):
926 status = ChangesetStatus.STATUS_REJECTED
926 status = ChangesetStatus.STATUS_REJECTED
927
927
928 if not message:
928 if not message:
929 message = (
929 message = (
930 _('Status change %(transition_icon)s %(status)s') % {
930 _('Status change %(transition_icon)s %(status)s') % {
931 'transition_icon': '>',
931 'transition_icon': '>',
932 'status': ChangesetStatus.get_status_lbl(status)})
932 'status': ChangesetStatus.get_status_lbl(status)})
933
933
934 internal_message = _('Closing with') + ' ' + message
934 internal_message = _('Closing with') + ' ' + message
935
935
936 comm = ChangesetCommentsModel().create(
936 comm = ChangesetCommentsModel().create(
937 text=internal_message,
937 text=internal_message,
938 repo=repo.repo_id,
938 repo=repo.repo_id,
939 user=user.user_id,
939 user=user.user_id,
940 pull_request=pull_request.pull_request_id,
940 pull_request=pull_request.pull_request_id,
941 f_path=None,
941 f_path=None,
942 line_no=None,
942 line_no=None,
943 status_change=ChangesetStatus.get_status_lbl(status),
943 status_change=ChangesetStatus.get_status_lbl(status),
944 status_change_type=status,
944 status_change_type=status,
945 closing_pr=True
945 closing_pr=True
946 )
946 )
947
947
948 ChangesetStatusModel().set_status(
948 ChangesetStatusModel().set_status(
949 repo.repo_id,
949 repo.repo_id,
950 status,
950 status,
951 user.user_id,
951 user.user_id,
952 comm,
952 comm,
953 pull_request=pull_request.pull_request_id
953 pull_request=pull_request.pull_request_id
954 )
954 )
955 Session().flush()
955 Session().flush()
956
956
957 PullRequestModel().close_pull_request(
957 PullRequestModel().close_pull_request(
958 pull_request.pull_request_id, user)
958 pull_request.pull_request_id, user)
959
959
960 def merge_status(self, pull_request):
960 def merge_status(self, pull_request):
961 if not self._is_merge_enabled(pull_request):
961 if not self._is_merge_enabled(pull_request):
962 return False, _('Server-side pull request merging is disabled.')
962 return False, _('Server-side pull request merging is disabled.')
963 if pull_request.is_closed():
963 if pull_request.is_closed():
964 return False, _('This pull request is closed.')
964 return False, _('This pull request is closed.')
965 merge_possible, msg = self._check_repo_requirements(
965 merge_possible, msg = self._check_repo_requirements(
966 target=pull_request.target_repo, source=pull_request.source_repo)
966 target=pull_request.target_repo, source=pull_request.source_repo)
967 if not merge_possible:
967 if not merge_possible:
968 return merge_possible, msg
968 return merge_possible, msg
969
969
970 try:
970 try:
971 resp = self._try_merge(pull_request)
971 resp = self._try_merge(pull_request)
972 log.debug("Merge response: %s", resp)
972 log.debug("Merge response: %s", resp)
973 status = resp.possible, self.merge_status_message(
973 status = resp.possible, self.merge_status_message(
974 resp.failure_reason)
974 resp.failure_reason)
975 except NotImplementedError:
975 except NotImplementedError:
976 status = False, _('Pull request merging is not supported.')
976 status = False, _('Pull request merging is not supported.')
977
977
978 return status
978 return status
979
979
980 def _check_repo_requirements(self, target, source):
980 def _check_repo_requirements(self, target, source):
981 """
981 """
982 Check if `target` and `source` have compatible requirements.
982 Check if `target` and `source` have compatible requirements.
983
983
984 Currently this is just checking for largefiles.
984 Currently this is just checking for largefiles.
985 """
985 """
986 target_has_largefiles = self._has_largefiles(target)
986 target_has_largefiles = self._has_largefiles(target)
987 source_has_largefiles = self._has_largefiles(source)
987 source_has_largefiles = self._has_largefiles(source)
988 merge_possible = True
988 merge_possible = True
989 message = u''
989 message = u''
990
990
991 if target_has_largefiles != source_has_largefiles:
991 if target_has_largefiles != source_has_largefiles:
992 merge_possible = False
992 merge_possible = False
993 if source_has_largefiles:
993 if source_has_largefiles:
994 message = _(
994 message = _(
995 'Target repository large files support is disabled.')
995 'Target repository large files support is disabled.')
996 else:
996 else:
997 message = _(
997 message = _(
998 'Source repository large files support is disabled.')
998 'Source repository large files support is disabled.')
999
999
1000 return merge_possible, message
1000 return merge_possible, message
1001
1001
1002 def _has_largefiles(self, repo):
1002 def _has_largefiles(self, repo):
1003 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1003 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1004 'extensions', 'largefiles')
1004 'extensions', 'largefiles')
1005 return largefiles_ui and largefiles_ui[0].active
1005 return largefiles_ui and largefiles_ui[0].active
1006
1006
1007 def _try_merge(self, pull_request):
1007 def _try_merge(self, pull_request):
1008 """
1008 """
1009 Try to merge the pull request and return the merge status.
1009 Try to merge the pull request and return the merge status.
1010 """
1010 """
1011 log.debug(
1011 log.debug(
1012 "Trying out if the pull request %s can be merged.",
1012 "Trying out if the pull request %s can be merged.",
1013 pull_request.pull_request_id)
1013 pull_request.pull_request_id)
1014 target_vcs = pull_request.target_repo.scm_instance()
1014 target_vcs = pull_request.target_repo.scm_instance()
1015
1015
1016 # Refresh the target reference.
1016 # Refresh the target reference.
1017 try:
1017 try:
1018 target_ref = self._refresh_reference(
1018 target_ref = self._refresh_reference(
1019 pull_request.target_ref_parts, target_vcs)
1019 pull_request.target_ref_parts, target_vcs)
1020 except CommitDoesNotExistError:
1020 except CommitDoesNotExistError:
1021 merge_state = MergeResponse(
1021 merge_state = MergeResponse(
1022 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1022 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1023 return merge_state
1023 return merge_state
1024
1024
1025 target_locked = pull_request.target_repo.locked
1025 target_locked = pull_request.target_repo.locked
1026 if target_locked and target_locked[0]:
1026 if target_locked and target_locked[0]:
1027 log.debug("The target repository is locked.")
1027 log.debug("The target repository is locked.")
1028 merge_state = MergeResponse(
1028 merge_state = MergeResponse(
1029 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1029 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1030 elif self._needs_merge_state_refresh(pull_request, target_ref):
1030 elif self._needs_merge_state_refresh(pull_request, target_ref):
1031 log.debug("Refreshing the merge status of the repository.")
1031 log.debug("Refreshing the merge status of the repository.")
1032 merge_state = self._refresh_merge_state(
1032 merge_state = self._refresh_merge_state(
1033 pull_request, target_vcs, target_ref)
1033 pull_request, target_vcs, target_ref)
1034 else:
1034 else:
1035 possible = pull_request.\
1035 possible = pull_request.\
1036 _last_merge_status == MergeFailureReason.NONE
1036 _last_merge_status == MergeFailureReason.NONE
1037 merge_state = MergeResponse(
1037 merge_state = MergeResponse(
1038 possible, False, None, pull_request._last_merge_status)
1038 possible, False, None, pull_request._last_merge_status)
1039
1039
1040 return merge_state
1040 return merge_state
1041
1041
1042 def _refresh_reference(self, reference, vcs_repository):
1042 def _refresh_reference(self, reference, vcs_repository):
1043 if reference.type in ('branch', 'book'):
1043 if reference.type in ('branch', 'book'):
1044 name_or_id = reference.name
1044 name_or_id = reference.name
1045 else:
1045 else:
1046 name_or_id = reference.commit_id
1046 name_or_id = reference.commit_id
1047 refreshed_commit = vcs_repository.get_commit(name_or_id)
1047 refreshed_commit = vcs_repository.get_commit(name_or_id)
1048 refreshed_reference = Reference(
1048 refreshed_reference = Reference(
1049 reference.type, reference.name, refreshed_commit.raw_id)
1049 reference.type, reference.name, refreshed_commit.raw_id)
1050 return refreshed_reference
1050 return refreshed_reference
1051
1051
1052 def _needs_merge_state_refresh(self, pull_request, target_reference):
1052 def _needs_merge_state_refresh(self, pull_request, target_reference):
1053 return not(
1053 return not(
1054 pull_request.revisions and
1054 pull_request.revisions and
1055 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1055 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1056 target_reference.commit_id == pull_request._last_merge_target_rev)
1056 target_reference.commit_id == pull_request._last_merge_target_rev)
1057
1057
1058 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1058 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1059 workspace_id = self._workspace_id(pull_request)
1059 workspace_id = self._workspace_id(pull_request)
1060 source_vcs = pull_request.source_repo.scm_instance()
1060 source_vcs = pull_request.source_repo.scm_instance()
1061 use_rebase = self._use_rebase_for_merging(pull_request)
1061 use_rebase = self._use_rebase_for_merging(pull_request)
1062 merge_state = target_vcs.merge(
1062 merge_state = target_vcs.merge(
1063 target_reference, source_vcs, pull_request.source_ref_parts,
1063 target_reference, source_vcs, pull_request.source_ref_parts,
1064 workspace_id, dry_run=True, use_rebase=use_rebase)
1064 workspace_id, dry_run=True, use_rebase=use_rebase)
1065
1065
1066 # Do not store the response if there was an unknown error.
1066 # Do not store the response if there was an unknown error.
1067 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1067 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1068 pull_request._last_merge_source_rev = \
1068 pull_request._last_merge_source_rev = \
1069 pull_request.source_ref_parts.commit_id
1069 pull_request.source_ref_parts.commit_id
1070 pull_request._last_merge_target_rev = target_reference.commit_id
1070 pull_request._last_merge_target_rev = target_reference.commit_id
1071 pull_request._last_merge_status = merge_state.failure_reason
1071 pull_request._last_merge_status = merge_state.failure_reason
1072 pull_request.shadow_merge_ref = merge_state.merge_ref
1072 pull_request.shadow_merge_ref = merge_state.merge_ref
1073 Session().add(pull_request)
1073 Session().add(pull_request)
1074 Session().commit()
1074 Session().commit()
1075
1075
1076 return merge_state
1076 return merge_state
1077
1077
1078 def _workspace_id(self, pull_request):
1078 def _workspace_id(self, pull_request):
1079 workspace_id = 'pr-%s' % pull_request.pull_request_id
1079 workspace_id = 'pr-%s' % pull_request.pull_request_id
1080 return workspace_id
1080 return workspace_id
1081
1081
1082 def merge_status_message(self, status_code):
1082 def merge_status_message(self, status_code):
1083 """
1083 """
1084 Return a human friendly error message for the given merge status code.
1084 Return a human friendly error message for the given merge status code.
1085 """
1085 """
1086 return self.MERGE_STATUS_MESSAGES[status_code]
1086 return self.MERGE_STATUS_MESSAGES[status_code]
1087
1087
1088 def generate_repo_data(self, repo, commit_id=None, branch=None,
1088 def generate_repo_data(self, repo, commit_id=None, branch=None,
1089 bookmark=None):
1089 bookmark=None):
1090 all_refs, selected_ref = \
1090 all_refs, selected_ref = \
1091 self._get_repo_pullrequest_sources(
1091 self._get_repo_pullrequest_sources(
1092 repo.scm_instance(), commit_id=commit_id,
1092 repo.scm_instance(), commit_id=commit_id,
1093 branch=branch, bookmark=bookmark)
1093 branch=branch, bookmark=bookmark)
1094
1094
1095 refs_select2 = []
1095 refs_select2 = []
1096 for element in all_refs:
1096 for element in all_refs:
1097 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1097 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1098 refs_select2.append({'text': element[1], 'children': children})
1098 refs_select2.append({'text': element[1], 'children': children})
1099
1099
1100 return {
1100 return {
1101 'user': {
1101 'user': {
1102 'user_id': repo.user.user_id,
1102 'user_id': repo.user.user_id,
1103 'username': repo.user.username,
1103 'username': repo.user.username,
1104 'firstname': repo.user.firstname,
1104 'firstname': repo.user.firstname,
1105 'lastname': repo.user.lastname,
1105 'lastname': repo.user.lastname,
1106 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1106 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1107 },
1107 },
1108 'description': h.chop_at_smart(repo.description, '\n'),
1108 'description': h.chop_at_smart(repo.description, '\n'),
1109 'refs': {
1109 'refs': {
1110 'all_refs': all_refs,
1110 'all_refs': all_refs,
1111 'selected_ref': selected_ref,
1111 'selected_ref': selected_ref,
1112 'select2_refs': refs_select2
1112 'select2_refs': refs_select2
1113 }
1113 }
1114 }
1114 }
1115
1115
1116 def generate_pullrequest_title(self, source, source_ref, target):
1116 def generate_pullrequest_title(self, source, source_ref, target):
1117 return u'{source}#{at_ref} to {target}'.format(
1117 return u'{source}#{at_ref} to {target}'.format(
1118 source=source,
1118 source=source,
1119 at_ref=source_ref,
1119 at_ref=source_ref,
1120 target=target,
1120 target=target,
1121 )
1121 )
1122
1122
1123 def _cleanup_merge_workspace(self, pull_request):
1123 def _cleanup_merge_workspace(self, pull_request):
1124 # Merging related cleanup
1124 # Merging related cleanup
1125 target_scm = pull_request.target_repo.scm_instance()
1125 target_scm = pull_request.target_repo.scm_instance()
1126 workspace_id = 'pr-%s' % pull_request.pull_request_id
1126 workspace_id = 'pr-%s' % pull_request.pull_request_id
1127
1127
1128 try:
1128 try:
1129 target_scm.cleanup_merge_workspace(workspace_id)
1129 target_scm.cleanup_merge_workspace(workspace_id)
1130 except NotImplementedError:
1130 except NotImplementedError:
1131 pass
1131 pass
1132
1132
1133 def _get_repo_pullrequest_sources(
1133 def _get_repo_pullrequest_sources(
1134 self, repo, commit_id=None, branch=None, bookmark=None):
1134 self, repo, commit_id=None, branch=None, bookmark=None):
1135 """
1135 """
1136 Return a structure with repo's interesting commits, suitable for
1136 Return a structure with repo's interesting commits, suitable for
1137 the selectors in pullrequest controller
1137 the selectors in pullrequest controller
1138
1138
1139 :param commit_id: a commit that must be in the list somehow
1139 :param commit_id: a commit that must be in the list somehow
1140 and selected by default
1140 and selected by default
1141 :param branch: a branch that must be in the list and selected
1141 :param branch: a branch that must be in the list and selected
1142 by default - even if closed
1142 by default - even if closed
1143 :param bookmark: a bookmark that must be in the list and selected
1143 :param bookmark: a bookmark that must be in the list and selected
1144 """
1144 """
1145
1145
1146 commit_id = safe_str(commit_id) if commit_id else None
1146 commit_id = safe_str(commit_id) if commit_id else None
1147 branch = safe_str(branch) if branch else None
1147 branch = safe_str(branch) if branch else None
1148 bookmark = safe_str(bookmark) if bookmark else None
1148 bookmark = safe_str(bookmark) if bookmark else None
1149
1149
1150 selected = None
1150 selected = None
1151
1151
1152 # order matters: first source that has commit_id in it will be selected
1152 # order matters: first source that has commit_id in it will be selected
1153 sources = []
1153 sources = []
1154 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1154 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1155 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1155 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1156
1156
1157 if commit_id:
1157 if commit_id:
1158 ref_commit = (h.short_id(commit_id), commit_id)
1158 ref_commit = (h.short_id(commit_id), commit_id)
1159 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1159 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1160
1160
1161 sources.append(
1161 sources.append(
1162 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1162 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1163 )
1163 )
1164
1164
1165 groups = []
1165 groups = []
1166 for group_key, ref_list, group_name, match in sources:
1166 for group_key, ref_list, group_name, match in sources:
1167 group_refs = []
1167 group_refs = []
1168 for ref_name, ref_id in ref_list:
1168 for ref_name, ref_id in ref_list:
1169 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1169 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1170 group_refs.append((ref_key, ref_name))
1170 group_refs.append((ref_key, ref_name))
1171
1171
1172 if not selected:
1172 if not selected:
1173 if set([commit_id, match]) & set([ref_id, ref_name]):
1173 if set([commit_id, match]) & set([ref_id, ref_name]):
1174 selected = ref_key
1174 selected = ref_key
1175
1175
1176 if group_refs:
1176 if group_refs:
1177 groups.append((group_refs, group_name))
1177 groups.append((group_refs, group_name))
1178
1178
1179 if not selected:
1179 if not selected:
1180 ref = commit_id or branch or bookmark
1180 ref = commit_id or branch or bookmark
1181 if ref:
1181 if ref:
1182 raise CommitDoesNotExistError(
1182 raise CommitDoesNotExistError(
1183 'No commit refs could be found matching: %s' % ref)
1183 'No commit refs could be found matching: %s' % ref)
1184 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1184 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1185 selected = 'branch:%s:%s' % (
1185 selected = 'branch:%s:%s' % (
1186 repo.DEFAULT_BRANCH_NAME,
1186 repo.DEFAULT_BRANCH_NAME,
1187 repo.branches[repo.DEFAULT_BRANCH_NAME]
1187 repo.branches[repo.DEFAULT_BRANCH_NAME]
1188 )
1188 )
1189 elif repo.commit_ids:
1189 elif repo.commit_ids:
1190 rev = repo.commit_ids[0]
1190 rev = repo.commit_ids[0]
1191 selected = 'rev:%s:%s' % (rev, rev)
1191 selected = 'rev:%s:%s' % (rev, rev)
1192 else:
1192 else:
1193 raise EmptyRepositoryError()
1193 raise EmptyRepositoryError()
1194 return groups, selected
1194 return groups, selected
1195
1195
1196 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1196 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1197 pull_request = self.__get_pull_request(pull_request)
1197 pull_request = self.__get_pull_request(pull_request)
1198 return self._get_diff_from_pr_or_version(pull_request, context=context)
1198 return self._get_diff_from_pr_or_version(pull_request, context=context)
1199
1199
1200 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1200 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1201 source_repo = pr_or_version.source_repo
1201 source_repo = pr_or_version.source_repo
1202
1202
1203 # we swap org/other ref since we run a simple diff on one repo
1203 # we swap org/other ref since we run a simple diff on one repo
1204 target_ref_id = pr_or_version.target_ref_parts.commit_id
1204 target_ref_id = pr_or_version.target_ref_parts.commit_id
1205 source_ref_id = pr_or_version.source_ref_parts.commit_id
1205 source_ref_id = pr_or_version.source_ref_parts.commit_id
1206 target_commit = source_repo.get_commit(
1206 target_commit = source_repo.get_commit(
1207 commit_id=safe_str(target_ref_id))
1207 commit_id=safe_str(target_ref_id))
1208 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1208 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1209 vcs_repo = source_repo.scm_instance()
1209 vcs_repo = source_repo.scm_instance()
1210
1210
1211 # TODO: johbo: In the context of an update, we cannot reach
1211 # TODO: johbo: In the context of an update, we cannot reach
1212 # the old commit anymore with our normal mechanisms. It needs
1212 # the old commit anymore with our normal mechanisms. It needs
1213 # some sort of special support in the vcs layer to avoid this
1213 # some sort of special support in the vcs layer to avoid this
1214 # workaround.
1214 # workaround.
1215 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1215 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1216 vcs_repo.alias == 'git'):
1216 vcs_repo.alias == 'git'):
1217 source_commit.raw_id = safe_str(source_ref_id)
1217 source_commit.raw_id = safe_str(source_ref_id)
1218
1218
1219 log.debug('calculating diff between '
1219 log.debug('calculating diff between '
1220 'source_ref:%s and target_ref:%s for repo `%s`',
1220 'source_ref:%s and target_ref:%s for repo `%s`',
1221 target_ref_id, source_ref_id,
1221 target_ref_id, source_ref_id,
1222 safe_unicode(vcs_repo.path))
1222 safe_unicode(vcs_repo.path))
1223
1223
1224 vcs_diff = vcs_repo.get_diff(
1224 vcs_diff = vcs_repo.get_diff(
1225 commit1=target_commit, commit2=source_commit, context=context)
1225 commit1=target_commit, commit2=source_commit, context=context)
1226 return vcs_diff
1226 return vcs_diff
1227
1227
1228 def _is_merge_enabled(self, pull_request):
1228 def _is_merge_enabled(self, pull_request):
1229 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1229 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1230 settings = settings_model.get_general_settings()
1230 settings = settings_model.get_general_settings()
1231 return settings.get('rhodecode_pr_merge_enabled', False)
1231 return settings.get('rhodecode_pr_merge_enabled', False)
1232
1232
1233 def _use_rebase_for_merging(self, pull_request):
1233 def _use_rebase_for_merging(self, pull_request):
1234 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1234 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1235 settings = settings_model.get_general_settings()
1235 settings = settings_model.get_general_settings()
1236 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1236 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1237
1237
1238 def _log_action(self, action, user, pull_request):
1238 def _log_action(self, action, user, pull_request):
1239 action_logger(
1239 action_logger(
1240 user,
1240 user,
1241 '{action}:{pr_id}'.format(
1241 '{action}:{pr_id}'.format(
1242 action=action, pr_id=pull_request.pull_request_id),
1242 action=action, pr_id=pull_request.pull_request_id),
1243 pull_request.target_repo)
1243 pull_request.target_repo)
1244
1244
1245
1245
1246 ChangeTuple = namedtuple('ChangeTuple',
1246 ChangeTuple = namedtuple('ChangeTuple',
1247 ['added', 'common', 'removed'])
1247 ['added', 'common', 'removed'])
1248
1248
1249 FileChangeTuple = namedtuple('FileChangeTuple',
1249 FileChangeTuple = namedtuple('FileChangeTuple',
1250 ['added', 'modified', 'removed'])
1250 ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now