##// END OF EJS Templates
default-reviewers: diff data should load more things lazy for better performance.
marcink -
r4508:2365f5e7 stable
parent child Browse files
Show More
@@ -1,2201 +1,2205 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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
26
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30
30
31 import datetime
31 import datetime
32 import urllib
32 import urllib
33 import collections
33 import collections
34
34
35 from pyramid import compat
35 from pyramid import compat
36 from pyramid.threadlocal import get_current_request
36 from pyramid.threadlocal import get_current_request
37
37
38 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39 from rhodecode.translation import lazy_ugettext
39 from rhodecode.translation import lazy_ugettext
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 from rhodecode.lib import audit_logger
41 from rhodecode.lib import audit_logger
42 from rhodecode.lib.compat import OrderedDict
42 from rhodecode.lib.compat import OrderedDict
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.markup_renderer import (
44 from rhodecode.lib.markup_renderer import (
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 from rhodecode.lib.utils2 import (
46 from rhodecode.lib.utils2 import (
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 get_current_rhodecode_user)
48 get_current_rhodecode_user)
49 from rhodecode.lib.vcs.backends.base import (
49 from rhodecode.lib.vcs.backends.base import (
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
51 TargetRefMissing, SourceRefMissing)
51 TargetRefMissing, SourceRefMissing)
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 CommitDoesNotExistError, EmptyRepositoryError)
54 CommitDoesNotExistError, EmptyRepositoryError)
55 from rhodecode.model import BaseModel
55 from rhodecode.model import BaseModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62 from rhodecode.model.notification import NotificationModel, \
62 from rhodecode.model.notification import NotificationModel, \
63 EmailNotificationModel
63 EmailNotificationModel
64 from rhodecode.model.scm import ScmModel
64 from rhodecode.model.scm import ScmModel
65 from rhodecode.model.settings import VcsSettingsModel
65 from rhodecode.model.settings import VcsSettingsModel
66
66
67
67
68 log = logging.getLogger(__name__)
68 log = logging.getLogger(__name__)
69
69
70
70
71 # Data structure to hold the response data when updating commits during a pull
71 # Data structure to hold the response data when updating commits during a pull
72 # request update.
72 # request update.
73 class UpdateResponse(object):
73 class UpdateResponse(object):
74
74
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
76 commit_changes, source_changed, target_changed):
76 commit_changes, source_changed, target_changed):
77
77
78 self.executed = executed
78 self.executed = executed
79 self.reason = reason
79 self.reason = reason
80 self.new = new
80 self.new = new
81 self.old = old
81 self.old = old
82 self.common_ancestor_id = common_ancestor_id
82 self.common_ancestor_id = common_ancestor_id
83 self.changes = commit_changes
83 self.changes = commit_changes
84 self.source_changed = source_changed
84 self.source_changed = source_changed
85 self.target_changed = target_changed
85 self.target_changed = target_changed
86
86
87
87
88 def get_diff_info(
88 def get_diff_info(
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
90 get_commit_authors=True):
90 get_commit_authors=True):
91 """
91 """
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
93 This is also used for default reviewers logic
93 This is also used for default reviewers logic
94 """
94 """
95
95
96 source_scm = source_repo.scm_instance()
96 source_scm = source_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
98
98
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
100 if not ancestor_id:
100 if not ancestor_id:
101 raise ValueError(
101 raise ValueError(
102 'cannot calculate diff info without a common ancestor. '
102 'cannot calculate diff info without a common ancestor. '
103 'Make sure both repositories are related, and have a common forking commit.')
103 'Make sure both repositories are related, and have a common forking commit.')
104
104
105 # case here is that want a simple diff without incoming commits,
105 # case here is that want a simple diff without incoming commits,
106 # previewing what will be merged based only on commits in the source.
106 # previewing what will be merged based only on commits in the source.
107 log.debug('Using ancestor %s as source_ref instead of %s',
107 log.debug('Using ancestor %s as source_ref instead of %s',
108 ancestor_id, source_ref)
108 ancestor_id, source_ref)
109
109
110 # source of changes now is the common ancestor
110 # source of changes now is the common ancestor
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
112 # target commit becomes the source ref as it is the last commit
112 # target commit becomes the source ref as it is the last commit
113 # for diff generation this logic gives proper diff
113 # for diff generation this logic gives proper diff
114 target_commit = source_scm.get_commit(commit_id=source_ref)
114 target_commit = source_scm.get_commit(commit_id=source_ref)
115
115
116 vcs_diff = \
116 vcs_diff = \
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
118 ignore_whitespace=False, context=3)
118 ignore_whitespace=False, context=3)
119
119
120 diff_processor = diffs.DiffProcessor(
120 diff_processor = diffs.DiffProcessor(
121 vcs_diff, format='newdiff', diff_limit=None,
121 vcs_diff, format='newdiff', diff_limit=None,
122 file_limit=None, show_full_diff=True)
122 file_limit=None, show_full_diff=True)
123
123
124 _parsed = diff_processor.prepare()
124 _parsed = diff_processor.prepare()
125
125
126 all_files = []
126 all_files = []
127 all_files_changes = []
127 all_files_changes = []
128 changed_lines = {}
128 changed_lines = {}
129 stats = [0, 0]
129 stats = [0, 0]
130 for f in _parsed:
130 for f in _parsed:
131 all_files.append(f['filename'])
131 all_files.append(f['filename'])
132 all_files_changes.append({
132 all_files_changes.append({
133 'filename': f['filename'],
133 'filename': f['filename'],
134 'stats': f['stats']
134 'stats': f['stats']
135 })
135 })
136 stats[0] += f['stats']['added']
136 stats[0] += f['stats']['added']
137 stats[1] += f['stats']['deleted']
137 stats[1] += f['stats']['deleted']
138
138
139 changed_lines[f['filename']] = []
139 changed_lines[f['filename']] = []
140 if len(f['chunks']) < 2:
140 if len(f['chunks']) < 2:
141 continue
141 continue
142 # first line is "context" information
142 # first line is "context" information
143 for chunks in f['chunks'][1:]:
143 for chunks in f['chunks'][1:]:
144 for chunk in chunks['lines']:
144 for chunk in chunks['lines']:
145 if chunk['action'] not in ('del', 'mod'):
145 if chunk['action'] not in ('del', 'mod'):
146 continue
146 continue
147 changed_lines[f['filename']].append(chunk['old_lineno'])
147 changed_lines[f['filename']].append(chunk['old_lineno'])
148
148
149 commit_authors = []
149 commit_authors = []
150 user_counts = {}
150 user_counts = {}
151 email_counts = {}
151 email_counts = {}
152 author_counts = {}
152 author_counts = {}
153 _commit_cache = {}
153 _commit_cache = {}
154
154
155 commits = []
155 commits = []
156 if get_commit_authors:
156 if get_commit_authors:
157 log.debug('Obtaining commit authors from set of commits')
157 commits = target_scm.compare(
158 commits = target_scm.compare(
158 target_ref, source_ref, source_scm, merge=True,
159 target_ref, source_ref, source_scm, merge=True,
159 pre_load=["author"])
160 pre_load=["author", "date", "message", "branch", "parents"])
160
161
161 for commit in commits:
162 for commit in commits:
162 user = User.get_from_cs_author(commit.author)
163 user = User.get_from_cs_author(commit.author)
163 if user and user not in commit_authors:
164 if user and user not in commit_authors:
164 commit_authors.append(user)
165 commit_authors.append(user)
165
166
166 # lines
167 # lines
167 if get_authors:
168 if get_authors:
169 log.debug('Calculating authors of changed files')
168 target_commit = source_repo.get_commit(ancestor_id)
170 target_commit = source_repo.get_commit(ancestor_id)
169
171
170 for fname, lines in changed_lines.items():
172 for fname, lines in changed_lines.items():
171 try:
173 try:
172 node = target_commit.get_node(fname)
174 node = target_commit.get_node(fname)
173 except Exception:
175 except Exception:
174 continue
176 continue
175
177
176 if not isinstance(node, FileNode):
178 if not isinstance(node, FileNode):
177 continue
179 continue
178
180
179 for annotation in node.annotate:
181 for annotation in node.annotate:
180 line_no, commit_id, get_commit_func, line_text = annotation
182 line_no, commit_id, get_commit_func, line_text = annotation
181 if line_no in lines:
183 if line_no in lines:
182 if commit_id not in _commit_cache:
184 if commit_id not in _commit_cache:
183 _commit_cache[commit_id] = get_commit_func()
185 _commit_cache[commit_id] = get_commit_func()
184 commit = _commit_cache[commit_id]
186 commit = _commit_cache[commit_id]
185 author = commit.author
187 author = commit.author
186 email = commit.author_email
188 email = commit.author_email
187 user = User.get_from_cs_author(author)
189 user = User.get_from_cs_author(author)
188 if user:
190 if user:
189 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
191 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
190 author_counts[author] = author_counts.get(author, 0) + 1
192 author_counts[author] = author_counts.get(author, 0) + 1
191 email_counts[email] = email_counts.get(email, 0) + 1
193 email_counts[email] = email_counts.get(email, 0) + 1
192
194
195 log.debug('Default reviewers processing finished')
196
193 return {
197 return {
194 'commits': commits,
198 'commits': commits,
195 'files': all_files_changes,
199 'files': all_files_changes,
196 'stats': stats,
200 'stats': stats,
197 'ancestor': ancestor_id,
201 'ancestor': ancestor_id,
198 # original authors of modified files
202 # original authors of modified files
199 'original_authors': {
203 'original_authors': {
200 'users': user_counts,
204 'users': user_counts,
201 'authors': author_counts,
205 'authors': author_counts,
202 'emails': email_counts,
206 'emails': email_counts,
203 },
207 },
204 'commit_authors': commit_authors
208 'commit_authors': commit_authors
205 }
209 }
206
210
207
211
208 class PullRequestModel(BaseModel):
212 class PullRequestModel(BaseModel):
209
213
210 cls = PullRequest
214 cls = PullRequest
211
215
212 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
216 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
213
217
214 UPDATE_STATUS_MESSAGES = {
218 UPDATE_STATUS_MESSAGES = {
215 UpdateFailureReason.NONE: lazy_ugettext(
219 UpdateFailureReason.NONE: lazy_ugettext(
216 'Pull request update successful.'),
220 'Pull request update successful.'),
217 UpdateFailureReason.UNKNOWN: lazy_ugettext(
221 UpdateFailureReason.UNKNOWN: lazy_ugettext(
218 'Pull request update failed because of an unknown error.'),
222 'Pull request update failed because of an unknown error.'),
219 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
223 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
220 'No update needed because the source and target have not changed.'),
224 'No update needed because the source and target have not changed.'),
221 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
225 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
222 'Pull request cannot be updated because the reference type is '
226 'Pull request cannot be updated because the reference type is '
223 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
227 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
224 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
228 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
225 'This pull request cannot be updated because the target '
229 'This pull request cannot be updated because the target '
226 'reference is missing.'),
230 'reference is missing.'),
227 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
231 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
228 'This pull request cannot be updated because the source '
232 'This pull request cannot be updated because the source '
229 'reference is missing.'),
233 'reference is missing.'),
230 }
234 }
231 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
235 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
232 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
236 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
233
237
234 def __get_pull_request(self, pull_request):
238 def __get_pull_request(self, pull_request):
235 return self._get_instance((
239 return self._get_instance((
236 PullRequest, PullRequestVersion), pull_request)
240 PullRequest, PullRequestVersion), pull_request)
237
241
238 def _check_perms(self, perms, pull_request, user, api=False):
242 def _check_perms(self, perms, pull_request, user, api=False):
239 if not api:
243 if not api:
240 return h.HasRepoPermissionAny(*perms)(
244 return h.HasRepoPermissionAny(*perms)(
241 user=user, repo_name=pull_request.target_repo.repo_name)
245 user=user, repo_name=pull_request.target_repo.repo_name)
242 else:
246 else:
243 return h.HasRepoPermissionAnyApi(*perms)(
247 return h.HasRepoPermissionAnyApi(*perms)(
244 user=user, repo_name=pull_request.target_repo.repo_name)
248 user=user, repo_name=pull_request.target_repo.repo_name)
245
249
246 def check_user_read(self, pull_request, user, api=False):
250 def check_user_read(self, pull_request, user, api=False):
247 _perms = ('repository.admin', 'repository.write', 'repository.read',)
251 _perms = ('repository.admin', 'repository.write', 'repository.read',)
248 return self._check_perms(_perms, pull_request, user, api)
252 return self._check_perms(_perms, pull_request, user, api)
249
253
250 def check_user_merge(self, pull_request, user, api=False):
254 def check_user_merge(self, pull_request, user, api=False):
251 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
255 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
252 return self._check_perms(_perms, pull_request, user, api)
256 return self._check_perms(_perms, pull_request, user, api)
253
257
254 def check_user_update(self, pull_request, user, api=False):
258 def check_user_update(self, pull_request, user, api=False):
255 owner = user.user_id == pull_request.user_id
259 owner = user.user_id == pull_request.user_id
256 return self.check_user_merge(pull_request, user, api) or owner
260 return self.check_user_merge(pull_request, user, api) or owner
257
261
258 def check_user_delete(self, pull_request, user):
262 def check_user_delete(self, pull_request, user):
259 owner = user.user_id == pull_request.user_id
263 owner = user.user_id == pull_request.user_id
260 _perms = ('repository.admin',)
264 _perms = ('repository.admin',)
261 return self._check_perms(_perms, pull_request, user) or owner
265 return self._check_perms(_perms, pull_request, user) or owner
262
266
263 def check_user_change_status(self, pull_request, user, api=False):
267 def check_user_change_status(self, pull_request, user, api=False):
264 reviewer = user.user_id in [x.user_id for x in
268 reviewer = user.user_id in [x.user_id for x in
265 pull_request.reviewers]
269 pull_request.reviewers]
266 return self.check_user_update(pull_request, user, api) or reviewer
270 return self.check_user_update(pull_request, user, api) or reviewer
267
271
268 def check_user_comment(self, pull_request, user):
272 def check_user_comment(self, pull_request, user):
269 owner = user.user_id == pull_request.user_id
273 owner = user.user_id == pull_request.user_id
270 return self.check_user_read(pull_request, user) or owner
274 return self.check_user_read(pull_request, user) or owner
271
275
272 def get(self, pull_request):
276 def get(self, pull_request):
273 return self.__get_pull_request(pull_request)
277 return self.__get_pull_request(pull_request)
274
278
275 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
279 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
276 statuses=None, opened_by=None, order_by=None,
280 statuses=None, opened_by=None, order_by=None,
277 order_dir='desc', only_created=False):
281 order_dir='desc', only_created=False):
278 repo = None
282 repo = None
279 if repo_name:
283 if repo_name:
280 repo = self._get_repo(repo_name)
284 repo = self._get_repo(repo_name)
281
285
282 q = PullRequest.query()
286 q = PullRequest.query()
283
287
284 if search_q:
288 if search_q:
285 like_expression = u'%{}%'.format(safe_unicode(search_q))
289 like_expression = u'%{}%'.format(safe_unicode(search_q))
286 q = q.join(User)
290 q = q.join(User)
287 q = q.filter(or_(
291 q = q.filter(or_(
288 cast(PullRequest.pull_request_id, String).ilike(like_expression),
292 cast(PullRequest.pull_request_id, String).ilike(like_expression),
289 User.username.ilike(like_expression),
293 User.username.ilike(like_expression),
290 PullRequest.title.ilike(like_expression),
294 PullRequest.title.ilike(like_expression),
291 PullRequest.description.ilike(like_expression),
295 PullRequest.description.ilike(like_expression),
292 ))
296 ))
293
297
294 # source or target
298 # source or target
295 if repo and source:
299 if repo and source:
296 q = q.filter(PullRequest.source_repo == repo)
300 q = q.filter(PullRequest.source_repo == repo)
297 elif repo:
301 elif repo:
298 q = q.filter(PullRequest.target_repo == repo)
302 q = q.filter(PullRequest.target_repo == repo)
299
303
300 # closed,opened
304 # closed,opened
301 if statuses:
305 if statuses:
302 q = q.filter(PullRequest.status.in_(statuses))
306 q = q.filter(PullRequest.status.in_(statuses))
303
307
304 # opened by filter
308 # opened by filter
305 if opened_by:
309 if opened_by:
306 q = q.filter(PullRequest.user_id.in_(opened_by))
310 q = q.filter(PullRequest.user_id.in_(opened_by))
307
311
308 # only get those that are in "created" state
312 # only get those that are in "created" state
309 if only_created:
313 if only_created:
310 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
314 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
311
315
312 if order_by:
316 if order_by:
313 order_map = {
317 order_map = {
314 'name_raw': PullRequest.pull_request_id,
318 'name_raw': PullRequest.pull_request_id,
315 'id': PullRequest.pull_request_id,
319 'id': PullRequest.pull_request_id,
316 'title': PullRequest.title,
320 'title': PullRequest.title,
317 'updated_on_raw': PullRequest.updated_on,
321 'updated_on_raw': PullRequest.updated_on,
318 'target_repo': PullRequest.target_repo_id
322 'target_repo': PullRequest.target_repo_id
319 }
323 }
320 if order_dir == 'asc':
324 if order_dir == 'asc':
321 q = q.order_by(order_map[order_by].asc())
325 q = q.order_by(order_map[order_by].asc())
322 else:
326 else:
323 q = q.order_by(order_map[order_by].desc())
327 q = q.order_by(order_map[order_by].desc())
324
328
325 return q
329 return q
326
330
327 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
331 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
328 opened_by=None):
332 opened_by=None):
329 """
333 """
330 Count the number of pull requests for a specific repository.
334 Count the number of pull requests for a specific repository.
331
335
332 :param repo_name: target or source repo
336 :param repo_name: target or source repo
333 :param search_q: filter by text
337 :param search_q: filter by text
334 :param source: boolean flag to specify if repo_name refers to source
338 :param source: boolean flag to specify if repo_name refers to source
335 :param statuses: list of pull request statuses
339 :param statuses: list of pull request statuses
336 :param opened_by: author user of the pull request
340 :param opened_by: author user of the pull request
337 :returns: int number of pull requests
341 :returns: int number of pull requests
338 """
342 """
339 q = self._prepare_get_all_query(
343 q = self._prepare_get_all_query(
340 repo_name, search_q=search_q, source=source, statuses=statuses,
344 repo_name, search_q=search_q, source=source, statuses=statuses,
341 opened_by=opened_by)
345 opened_by=opened_by)
342
346
343 return q.count()
347 return q.count()
344
348
345 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
349 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
346 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
350 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
347 """
351 """
348 Get all pull requests for a specific repository.
352 Get all pull requests for a specific repository.
349
353
350 :param repo_name: target or source repo
354 :param repo_name: target or source repo
351 :param search_q: filter by text
355 :param search_q: filter by text
352 :param source: boolean flag to specify if repo_name refers to source
356 :param source: boolean flag to specify if repo_name refers to source
353 :param statuses: list of pull request statuses
357 :param statuses: list of pull request statuses
354 :param opened_by: author user of the pull request
358 :param opened_by: author user of the pull request
355 :param offset: pagination offset
359 :param offset: pagination offset
356 :param length: length of returned list
360 :param length: length of returned list
357 :param order_by: order of the returned list
361 :param order_by: order of the returned list
358 :param order_dir: 'asc' or 'desc' ordering direction
362 :param order_dir: 'asc' or 'desc' ordering direction
359 :returns: list of pull requests
363 :returns: list of pull requests
360 """
364 """
361 q = self._prepare_get_all_query(
365 q = self._prepare_get_all_query(
362 repo_name, search_q=search_q, source=source, statuses=statuses,
366 repo_name, search_q=search_q, source=source, statuses=statuses,
363 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
367 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
364
368
365 if length:
369 if length:
366 pull_requests = q.limit(length).offset(offset).all()
370 pull_requests = q.limit(length).offset(offset).all()
367 else:
371 else:
368 pull_requests = q.all()
372 pull_requests = q.all()
369
373
370 return pull_requests
374 return pull_requests
371
375
372 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
376 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
373 opened_by=None):
377 opened_by=None):
374 """
378 """
375 Count the number of pull requests for a specific repository that are
379 Count the number of pull requests for a specific repository that are
376 awaiting review.
380 awaiting review.
377
381
378 :param repo_name: target or source repo
382 :param repo_name: target or source repo
379 :param search_q: filter by text
383 :param search_q: filter by text
380 :param source: boolean flag to specify if repo_name refers to source
384 :param source: boolean flag to specify if repo_name refers to source
381 :param statuses: list of pull request statuses
385 :param statuses: list of pull request statuses
382 :param opened_by: author user of the pull request
386 :param opened_by: author user of the pull request
383 :returns: int number of pull requests
387 :returns: int number of pull requests
384 """
388 """
385 pull_requests = self.get_awaiting_review(
389 pull_requests = self.get_awaiting_review(
386 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
390 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
387
391
388 return len(pull_requests)
392 return len(pull_requests)
389
393
390 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
394 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
391 opened_by=None, offset=0, length=None,
395 opened_by=None, offset=0, length=None,
392 order_by=None, order_dir='desc'):
396 order_by=None, order_dir='desc'):
393 """
397 """
394 Get all pull requests for a specific repository that are awaiting
398 Get all pull requests for a specific repository that are awaiting
395 review.
399 review.
396
400
397 :param repo_name: target or source repo
401 :param repo_name: target or source repo
398 :param search_q: filter by text
402 :param search_q: filter by text
399 :param source: boolean flag to specify if repo_name refers to source
403 :param source: boolean flag to specify if repo_name refers to source
400 :param statuses: list of pull request statuses
404 :param statuses: list of pull request statuses
401 :param opened_by: author user of the pull request
405 :param opened_by: author user of the pull request
402 :param offset: pagination offset
406 :param offset: pagination offset
403 :param length: length of returned list
407 :param length: length of returned list
404 :param order_by: order of the returned list
408 :param order_by: order of the returned list
405 :param order_dir: 'asc' or 'desc' ordering direction
409 :param order_dir: 'asc' or 'desc' ordering direction
406 :returns: list of pull requests
410 :returns: list of pull requests
407 """
411 """
408 pull_requests = self.get_all(
412 pull_requests = self.get_all(
409 repo_name, search_q=search_q, source=source, statuses=statuses,
413 repo_name, search_q=search_q, source=source, statuses=statuses,
410 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
414 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
411
415
412 _filtered_pull_requests = []
416 _filtered_pull_requests = []
413 for pr in pull_requests:
417 for pr in pull_requests:
414 status = pr.calculated_review_status()
418 status = pr.calculated_review_status()
415 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
419 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
416 ChangesetStatus.STATUS_UNDER_REVIEW]:
420 ChangesetStatus.STATUS_UNDER_REVIEW]:
417 _filtered_pull_requests.append(pr)
421 _filtered_pull_requests.append(pr)
418 if length:
422 if length:
419 return _filtered_pull_requests[offset:offset+length]
423 return _filtered_pull_requests[offset:offset+length]
420 else:
424 else:
421 return _filtered_pull_requests
425 return _filtered_pull_requests
422
426
423 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
427 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
424 opened_by=None, user_id=None):
428 opened_by=None, user_id=None):
425 """
429 """
426 Count the number of pull requests for a specific repository that are
430 Count the number of pull requests for a specific repository that are
427 awaiting review from a specific user.
431 awaiting review from a specific user.
428
432
429 :param repo_name: target or source repo
433 :param repo_name: target or source repo
430 :param search_q: filter by text
434 :param search_q: filter by text
431 :param source: boolean flag to specify if repo_name refers to source
435 :param source: boolean flag to specify if repo_name refers to source
432 :param statuses: list of pull request statuses
436 :param statuses: list of pull request statuses
433 :param opened_by: author user of the pull request
437 :param opened_by: author user of the pull request
434 :param user_id: reviewer user of the pull request
438 :param user_id: reviewer user of the pull request
435 :returns: int number of pull requests
439 :returns: int number of pull requests
436 """
440 """
437 pull_requests = self.get_awaiting_my_review(
441 pull_requests = self.get_awaiting_my_review(
438 repo_name, search_q=search_q, source=source, statuses=statuses,
442 repo_name, search_q=search_q, source=source, statuses=statuses,
439 opened_by=opened_by, user_id=user_id)
443 opened_by=opened_by, user_id=user_id)
440
444
441 return len(pull_requests)
445 return len(pull_requests)
442
446
443 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
447 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
444 opened_by=None, user_id=None, offset=0,
448 opened_by=None, user_id=None, offset=0,
445 length=None, order_by=None, order_dir='desc'):
449 length=None, order_by=None, order_dir='desc'):
446 """
450 """
447 Get all pull requests for a specific repository that are awaiting
451 Get all pull requests for a specific repository that are awaiting
448 review from a specific user.
452 review from a specific user.
449
453
450 :param repo_name: target or source repo
454 :param repo_name: target or source repo
451 :param search_q: filter by text
455 :param search_q: filter by text
452 :param source: boolean flag to specify if repo_name refers to source
456 :param source: boolean flag to specify if repo_name refers to source
453 :param statuses: list of pull request statuses
457 :param statuses: list of pull request statuses
454 :param opened_by: author user of the pull request
458 :param opened_by: author user of the pull request
455 :param user_id: reviewer user of the pull request
459 :param user_id: reviewer user of the pull request
456 :param offset: pagination offset
460 :param offset: pagination offset
457 :param length: length of returned list
461 :param length: length of returned list
458 :param order_by: order of the returned list
462 :param order_by: order of the returned list
459 :param order_dir: 'asc' or 'desc' ordering direction
463 :param order_dir: 'asc' or 'desc' ordering direction
460 :returns: list of pull requests
464 :returns: list of pull requests
461 """
465 """
462 pull_requests = self.get_all(
466 pull_requests = self.get_all(
463 repo_name, search_q=search_q, source=source, statuses=statuses,
467 repo_name, search_q=search_q, source=source, statuses=statuses,
464 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
468 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
465
469
466 _my = PullRequestModel().get_not_reviewed(user_id)
470 _my = PullRequestModel().get_not_reviewed(user_id)
467 my_participation = []
471 my_participation = []
468 for pr in pull_requests:
472 for pr in pull_requests:
469 if pr in _my:
473 if pr in _my:
470 my_participation.append(pr)
474 my_participation.append(pr)
471 _filtered_pull_requests = my_participation
475 _filtered_pull_requests = my_participation
472 if length:
476 if length:
473 return _filtered_pull_requests[offset:offset+length]
477 return _filtered_pull_requests[offset:offset+length]
474 else:
478 else:
475 return _filtered_pull_requests
479 return _filtered_pull_requests
476
480
477 def get_not_reviewed(self, user_id):
481 def get_not_reviewed(self, user_id):
478 return [
482 return [
479 x.pull_request for x in PullRequestReviewers.query().filter(
483 x.pull_request for x in PullRequestReviewers.query().filter(
480 PullRequestReviewers.user_id == user_id).all()
484 PullRequestReviewers.user_id == user_id).all()
481 ]
485 ]
482
486
483 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
487 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
484 order_by=None, order_dir='desc'):
488 order_by=None, order_dir='desc'):
485 q = PullRequest.query()
489 q = PullRequest.query()
486 if user_id:
490 if user_id:
487 reviewers_subquery = Session().query(
491 reviewers_subquery = Session().query(
488 PullRequestReviewers.pull_request_id).filter(
492 PullRequestReviewers.pull_request_id).filter(
489 PullRequestReviewers.user_id == user_id).subquery()
493 PullRequestReviewers.user_id == user_id).subquery()
490 user_filter = or_(
494 user_filter = or_(
491 PullRequest.user_id == user_id,
495 PullRequest.user_id == user_id,
492 PullRequest.pull_request_id.in_(reviewers_subquery)
496 PullRequest.pull_request_id.in_(reviewers_subquery)
493 )
497 )
494 q = PullRequest.query().filter(user_filter)
498 q = PullRequest.query().filter(user_filter)
495
499
496 # closed,opened
500 # closed,opened
497 if statuses:
501 if statuses:
498 q = q.filter(PullRequest.status.in_(statuses))
502 q = q.filter(PullRequest.status.in_(statuses))
499
503
500 if query:
504 if query:
501 like_expression = u'%{}%'.format(safe_unicode(query))
505 like_expression = u'%{}%'.format(safe_unicode(query))
502 q = q.join(User)
506 q = q.join(User)
503 q = q.filter(or_(
507 q = q.filter(or_(
504 cast(PullRequest.pull_request_id, String).ilike(like_expression),
508 cast(PullRequest.pull_request_id, String).ilike(like_expression),
505 User.username.ilike(like_expression),
509 User.username.ilike(like_expression),
506 PullRequest.title.ilike(like_expression),
510 PullRequest.title.ilike(like_expression),
507 PullRequest.description.ilike(like_expression),
511 PullRequest.description.ilike(like_expression),
508 ))
512 ))
509 if order_by:
513 if order_by:
510 order_map = {
514 order_map = {
511 'name_raw': PullRequest.pull_request_id,
515 'name_raw': PullRequest.pull_request_id,
512 'title': PullRequest.title,
516 'title': PullRequest.title,
513 'updated_on_raw': PullRequest.updated_on,
517 'updated_on_raw': PullRequest.updated_on,
514 'target_repo': PullRequest.target_repo_id
518 'target_repo': PullRequest.target_repo_id
515 }
519 }
516 if order_dir == 'asc':
520 if order_dir == 'asc':
517 q = q.order_by(order_map[order_by].asc())
521 q = q.order_by(order_map[order_by].asc())
518 else:
522 else:
519 q = q.order_by(order_map[order_by].desc())
523 q = q.order_by(order_map[order_by].desc())
520
524
521 return q
525 return q
522
526
523 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
527 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
524 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
528 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
525 return q.count()
529 return q.count()
526
530
527 def get_im_participating_in(
531 def get_im_participating_in(
528 self, user_id=None, statuses=None, query='', offset=0,
532 self, user_id=None, statuses=None, query='', offset=0,
529 length=None, order_by=None, order_dir='desc'):
533 length=None, order_by=None, order_dir='desc'):
530 """
534 """
531 Get all Pull requests that i'm participating in, or i have opened
535 Get all Pull requests that i'm participating in, or i have opened
532 """
536 """
533
537
534 q = self._prepare_participating_query(
538 q = self._prepare_participating_query(
535 user_id, statuses=statuses, query=query, order_by=order_by,
539 user_id, statuses=statuses, query=query, order_by=order_by,
536 order_dir=order_dir)
540 order_dir=order_dir)
537
541
538 if length:
542 if length:
539 pull_requests = q.limit(length).offset(offset).all()
543 pull_requests = q.limit(length).offset(offset).all()
540 else:
544 else:
541 pull_requests = q.all()
545 pull_requests = q.all()
542
546
543 return pull_requests
547 return pull_requests
544
548
545 def get_versions(self, pull_request):
549 def get_versions(self, pull_request):
546 """
550 """
547 returns version of pull request sorted by ID descending
551 returns version of pull request sorted by ID descending
548 """
552 """
549 return PullRequestVersion.query()\
553 return PullRequestVersion.query()\
550 .filter(PullRequestVersion.pull_request == pull_request)\
554 .filter(PullRequestVersion.pull_request == pull_request)\
551 .order_by(PullRequestVersion.pull_request_version_id.asc())\
555 .order_by(PullRequestVersion.pull_request_version_id.asc())\
552 .all()
556 .all()
553
557
554 def get_pr_version(self, pull_request_id, version=None):
558 def get_pr_version(self, pull_request_id, version=None):
555 at_version = None
559 at_version = None
556
560
557 if version and version == 'latest':
561 if version and version == 'latest':
558 pull_request_ver = PullRequest.get(pull_request_id)
562 pull_request_ver = PullRequest.get(pull_request_id)
559 pull_request_obj = pull_request_ver
563 pull_request_obj = pull_request_ver
560 _org_pull_request_obj = pull_request_obj
564 _org_pull_request_obj = pull_request_obj
561 at_version = 'latest'
565 at_version = 'latest'
562 elif version:
566 elif version:
563 pull_request_ver = PullRequestVersion.get_or_404(version)
567 pull_request_ver = PullRequestVersion.get_or_404(version)
564 pull_request_obj = pull_request_ver
568 pull_request_obj = pull_request_ver
565 _org_pull_request_obj = pull_request_ver.pull_request
569 _org_pull_request_obj = pull_request_ver.pull_request
566 at_version = pull_request_ver.pull_request_version_id
570 at_version = pull_request_ver.pull_request_version_id
567 else:
571 else:
568 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
572 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
569 pull_request_id)
573 pull_request_id)
570
574
571 pull_request_display_obj = PullRequest.get_pr_display_object(
575 pull_request_display_obj = PullRequest.get_pr_display_object(
572 pull_request_obj, _org_pull_request_obj)
576 pull_request_obj, _org_pull_request_obj)
573
577
574 return _org_pull_request_obj, pull_request_obj, \
578 return _org_pull_request_obj, pull_request_obj, \
575 pull_request_display_obj, at_version
579 pull_request_display_obj, at_version
576
580
577 def create(self, created_by, source_repo, source_ref, target_repo,
581 def create(self, created_by, source_repo, source_ref, target_repo,
578 target_ref, revisions, reviewers, observers, title, description=None,
582 target_ref, revisions, reviewers, observers, title, description=None,
579 common_ancestor_id=None,
583 common_ancestor_id=None,
580 description_renderer=None,
584 description_renderer=None,
581 reviewer_data=None, translator=None, auth_user=None):
585 reviewer_data=None, translator=None, auth_user=None):
582 translator = translator or get_current_request().translate
586 translator = translator or get_current_request().translate
583
587
584 created_by_user = self._get_user(created_by)
588 created_by_user = self._get_user(created_by)
585 auth_user = auth_user or created_by_user.AuthUser()
589 auth_user = auth_user or created_by_user.AuthUser()
586 source_repo = self._get_repo(source_repo)
590 source_repo = self._get_repo(source_repo)
587 target_repo = self._get_repo(target_repo)
591 target_repo = self._get_repo(target_repo)
588
592
589 pull_request = PullRequest()
593 pull_request = PullRequest()
590 pull_request.source_repo = source_repo
594 pull_request.source_repo = source_repo
591 pull_request.source_ref = source_ref
595 pull_request.source_ref = source_ref
592 pull_request.target_repo = target_repo
596 pull_request.target_repo = target_repo
593 pull_request.target_ref = target_ref
597 pull_request.target_ref = target_ref
594 pull_request.revisions = revisions
598 pull_request.revisions = revisions
595 pull_request.title = title
599 pull_request.title = title
596 pull_request.description = description
600 pull_request.description = description
597 pull_request.description_renderer = description_renderer
601 pull_request.description_renderer = description_renderer
598 pull_request.author = created_by_user
602 pull_request.author = created_by_user
599 pull_request.reviewer_data = reviewer_data
603 pull_request.reviewer_data = reviewer_data
600 pull_request.pull_request_state = pull_request.STATE_CREATING
604 pull_request.pull_request_state = pull_request.STATE_CREATING
601 pull_request.common_ancestor_id = common_ancestor_id
605 pull_request.common_ancestor_id = common_ancestor_id
602
606
603 Session().add(pull_request)
607 Session().add(pull_request)
604 Session().flush()
608 Session().flush()
605
609
606 reviewer_ids = set()
610 reviewer_ids = set()
607 # members / reviewers
611 # members / reviewers
608 for reviewer_object in reviewers:
612 for reviewer_object in reviewers:
609 user_id, reasons, mandatory, role, rules = reviewer_object
613 user_id, reasons, mandatory, role, rules = reviewer_object
610 user = self._get_user(user_id)
614 user = self._get_user(user_id)
611
615
612 # skip duplicates
616 # skip duplicates
613 if user.user_id in reviewer_ids:
617 if user.user_id in reviewer_ids:
614 continue
618 continue
615
619
616 reviewer_ids.add(user.user_id)
620 reviewer_ids.add(user.user_id)
617
621
618 reviewer = PullRequestReviewers()
622 reviewer = PullRequestReviewers()
619 reviewer.user = user
623 reviewer.user = user
620 reviewer.pull_request = pull_request
624 reviewer.pull_request = pull_request
621 reviewer.reasons = reasons
625 reviewer.reasons = reasons
622 reviewer.mandatory = mandatory
626 reviewer.mandatory = mandatory
623 reviewer.role = role
627 reviewer.role = role
624
628
625 # NOTE(marcink): pick only first rule for now
629 # NOTE(marcink): pick only first rule for now
626 rule_id = list(rules)[0] if rules else None
630 rule_id = list(rules)[0] if rules else None
627 rule = RepoReviewRule.get(rule_id) if rule_id else None
631 rule = RepoReviewRule.get(rule_id) if rule_id else None
628 if rule:
632 if rule:
629 review_group = rule.user_group_vote_rule(user_id)
633 review_group = rule.user_group_vote_rule(user_id)
630 # we check if this particular reviewer is member of a voting group
634 # we check if this particular reviewer is member of a voting group
631 if review_group:
635 if review_group:
632 # NOTE(marcink):
636 # NOTE(marcink):
633 # can be that user is member of more but we pick the first same,
637 # can be that user is member of more but we pick the first same,
634 # same as default reviewers algo
638 # same as default reviewers algo
635 review_group = review_group[0]
639 review_group = review_group[0]
636
640
637 rule_data = {
641 rule_data = {
638 'rule_name':
642 'rule_name':
639 rule.review_rule_name,
643 rule.review_rule_name,
640 'rule_user_group_entry_id':
644 'rule_user_group_entry_id':
641 review_group.repo_review_rule_users_group_id,
645 review_group.repo_review_rule_users_group_id,
642 'rule_user_group_name':
646 'rule_user_group_name':
643 review_group.users_group.users_group_name,
647 review_group.users_group.users_group_name,
644 'rule_user_group_members':
648 'rule_user_group_members':
645 [x.user.username for x in review_group.users_group.members],
649 [x.user.username for x in review_group.users_group.members],
646 'rule_user_group_members_id':
650 'rule_user_group_members_id':
647 [x.user.user_id for x in review_group.users_group.members],
651 [x.user.user_id for x in review_group.users_group.members],
648 }
652 }
649 # e.g {'vote_rule': -1, 'mandatory': True}
653 # e.g {'vote_rule': -1, 'mandatory': True}
650 rule_data.update(review_group.rule_data())
654 rule_data.update(review_group.rule_data())
651
655
652 reviewer.rule_data = rule_data
656 reviewer.rule_data = rule_data
653
657
654 Session().add(reviewer)
658 Session().add(reviewer)
655 Session().flush()
659 Session().flush()
656
660
657 for observer_object in observers:
661 for observer_object in observers:
658 user_id, reasons, mandatory, role, rules = observer_object
662 user_id, reasons, mandatory, role, rules = observer_object
659 user = self._get_user(user_id)
663 user = self._get_user(user_id)
660
664
661 # skip duplicates from reviewers
665 # skip duplicates from reviewers
662 if user.user_id in reviewer_ids:
666 if user.user_id in reviewer_ids:
663 continue
667 continue
664
668
665 #reviewer_ids.add(user.user_id)
669 #reviewer_ids.add(user.user_id)
666
670
667 observer = PullRequestReviewers()
671 observer = PullRequestReviewers()
668 observer.user = user
672 observer.user = user
669 observer.pull_request = pull_request
673 observer.pull_request = pull_request
670 observer.reasons = reasons
674 observer.reasons = reasons
671 observer.mandatory = mandatory
675 observer.mandatory = mandatory
672 observer.role = role
676 observer.role = role
673
677
674 # NOTE(marcink): pick only first rule for now
678 # NOTE(marcink): pick only first rule for now
675 rule_id = list(rules)[0] if rules else None
679 rule_id = list(rules)[0] if rules else None
676 rule = RepoReviewRule.get(rule_id) if rule_id else None
680 rule = RepoReviewRule.get(rule_id) if rule_id else None
677 if rule:
681 if rule:
678 # TODO(marcink): do we need this for observers ??
682 # TODO(marcink): do we need this for observers ??
679 pass
683 pass
680
684
681 Session().add(observer)
685 Session().add(observer)
682 Session().flush()
686 Session().flush()
683
687
684 # Set approval status to "Under Review" for all commits which are
688 # Set approval status to "Under Review" for all commits which are
685 # part of this pull request.
689 # part of this pull request.
686 ChangesetStatusModel().set_status(
690 ChangesetStatusModel().set_status(
687 repo=target_repo,
691 repo=target_repo,
688 status=ChangesetStatus.STATUS_UNDER_REVIEW,
692 status=ChangesetStatus.STATUS_UNDER_REVIEW,
689 user=created_by_user,
693 user=created_by_user,
690 pull_request=pull_request
694 pull_request=pull_request
691 )
695 )
692 # we commit early at this point. This has to do with a fact
696 # we commit early at this point. This has to do with a fact
693 # that before queries do some row-locking. And because of that
697 # that before queries do some row-locking. And because of that
694 # we need to commit and finish transaction before below validate call
698 # we need to commit and finish transaction before below validate call
695 # that for large repos could be long resulting in long row locks
699 # that for large repos could be long resulting in long row locks
696 Session().commit()
700 Session().commit()
697
701
698 # prepare workspace, and run initial merge simulation. Set state during that
702 # prepare workspace, and run initial merge simulation. Set state during that
699 # operation
703 # operation
700 pull_request = PullRequest.get(pull_request.pull_request_id)
704 pull_request = PullRequest.get(pull_request.pull_request_id)
701
705
702 # set as merging, for merge simulation, and if finished to created so we mark
706 # set as merging, for merge simulation, and if finished to created so we mark
703 # simulation is working fine
707 # simulation is working fine
704 with pull_request.set_state(PullRequest.STATE_MERGING,
708 with pull_request.set_state(PullRequest.STATE_MERGING,
705 final_state=PullRequest.STATE_CREATED) as state_obj:
709 final_state=PullRequest.STATE_CREATED) as state_obj:
706 MergeCheck.validate(
710 MergeCheck.validate(
707 pull_request, auth_user=auth_user, translator=translator)
711 pull_request, auth_user=auth_user, translator=translator)
708
712
709 self.notify_reviewers(pull_request, reviewer_ids)
713 self.notify_reviewers(pull_request, reviewer_ids)
710 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
714 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
711
715
712 creation_data = pull_request.get_api_data(with_merge_state=False)
716 creation_data = pull_request.get_api_data(with_merge_state=False)
713 self._log_audit_action(
717 self._log_audit_action(
714 'repo.pull_request.create', {'data': creation_data},
718 'repo.pull_request.create', {'data': creation_data},
715 auth_user, pull_request)
719 auth_user, pull_request)
716
720
717 return pull_request
721 return pull_request
718
722
719 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
723 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
720 pull_request = self.__get_pull_request(pull_request)
724 pull_request = self.__get_pull_request(pull_request)
721 target_scm = pull_request.target_repo.scm_instance()
725 target_scm = pull_request.target_repo.scm_instance()
722 if action == 'create':
726 if action == 'create':
723 trigger_hook = hooks_utils.trigger_create_pull_request_hook
727 trigger_hook = hooks_utils.trigger_create_pull_request_hook
724 elif action == 'merge':
728 elif action == 'merge':
725 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
729 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
726 elif action == 'close':
730 elif action == 'close':
727 trigger_hook = hooks_utils.trigger_close_pull_request_hook
731 trigger_hook = hooks_utils.trigger_close_pull_request_hook
728 elif action == 'review_status_change':
732 elif action == 'review_status_change':
729 trigger_hook = hooks_utils.trigger_review_pull_request_hook
733 trigger_hook = hooks_utils.trigger_review_pull_request_hook
730 elif action == 'update':
734 elif action == 'update':
731 trigger_hook = hooks_utils.trigger_update_pull_request_hook
735 trigger_hook = hooks_utils.trigger_update_pull_request_hook
732 elif action == 'comment':
736 elif action == 'comment':
733 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
737 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
734 elif action == 'comment_edit':
738 elif action == 'comment_edit':
735 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
739 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
736 else:
740 else:
737 return
741 return
738
742
739 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
743 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
740 pull_request, action, trigger_hook)
744 pull_request, action, trigger_hook)
741 trigger_hook(
745 trigger_hook(
742 username=user.username,
746 username=user.username,
743 repo_name=pull_request.target_repo.repo_name,
747 repo_name=pull_request.target_repo.repo_name,
744 repo_type=target_scm.alias,
748 repo_type=target_scm.alias,
745 pull_request=pull_request,
749 pull_request=pull_request,
746 data=data)
750 data=data)
747
751
748 def _get_commit_ids(self, pull_request):
752 def _get_commit_ids(self, pull_request):
749 """
753 """
750 Return the commit ids of the merged pull request.
754 Return the commit ids of the merged pull request.
751
755
752 This method is not dealing correctly yet with the lack of autoupdates
756 This method is not dealing correctly yet with the lack of autoupdates
753 nor with the implicit target updates.
757 nor with the implicit target updates.
754 For example: if a commit in the source repo is already in the target it
758 For example: if a commit in the source repo is already in the target it
755 will be reported anyways.
759 will be reported anyways.
756 """
760 """
757 merge_rev = pull_request.merge_rev
761 merge_rev = pull_request.merge_rev
758 if merge_rev is None:
762 if merge_rev is None:
759 raise ValueError('This pull request was not merged yet')
763 raise ValueError('This pull request was not merged yet')
760
764
761 commit_ids = list(pull_request.revisions)
765 commit_ids = list(pull_request.revisions)
762 if merge_rev not in commit_ids:
766 if merge_rev not in commit_ids:
763 commit_ids.append(merge_rev)
767 commit_ids.append(merge_rev)
764
768
765 return commit_ids
769 return commit_ids
766
770
767 def merge_repo(self, pull_request, user, extras):
771 def merge_repo(self, pull_request, user, extras):
768 log.debug("Merging pull request %s", pull_request.pull_request_id)
772 log.debug("Merging pull request %s", pull_request.pull_request_id)
769 extras['user_agent'] = 'internal-merge'
773 extras['user_agent'] = 'internal-merge'
770 merge_state = self._merge_pull_request(pull_request, user, extras)
774 merge_state = self._merge_pull_request(pull_request, user, extras)
771 if merge_state.executed:
775 if merge_state.executed:
772 log.debug("Merge was successful, updating the pull request comments.")
776 log.debug("Merge was successful, updating the pull request comments.")
773 self._comment_and_close_pr(pull_request, user, merge_state)
777 self._comment_and_close_pr(pull_request, user, merge_state)
774
778
775 self._log_audit_action(
779 self._log_audit_action(
776 'repo.pull_request.merge',
780 'repo.pull_request.merge',
777 {'merge_state': merge_state.__dict__},
781 {'merge_state': merge_state.__dict__},
778 user, pull_request)
782 user, pull_request)
779
783
780 else:
784 else:
781 log.warn("Merge failed, not updating the pull request.")
785 log.warn("Merge failed, not updating the pull request.")
782 return merge_state
786 return merge_state
783
787
784 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
788 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
785 target_vcs = pull_request.target_repo.scm_instance()
789 target_vcs = pull_request.target_repo.scm_instance()
786 source_vcs = pull_request.source_repo.scm_instance()
790 source_vcs = pull_request.source_repo.scm_instance()
787
791
788 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
792 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
789 pr_id=pull_request.pull_request_id,
793 pr_id=pull_request.pull_request_id,
790 pr_title=pull_request.title,
794 pr_title=pull_request.title,
791 source_repo=source_vcs.name,
795 source_repo=source_vcs.name,
792 source_ref_name=pull_request.source_ref_parts.name,
796 source_ref_name=pull_request.source_ref_parts.name,
793 target_repo=target_vcs.name,
797 target_repo=target_vcs.name,
794 target_ref_name=pull_request.target_ref_parts.name,
798 target_ref_name=pull_request.target_ref_parts.name,
795 )
799 )
796
800
797 workspace_id = self._workspace_id(pull_request)
801 workspace_id = self._workspace_id(pull_request)
798 repo_id = pull_request.target_repo.repo_id
802 repo_id = pull_request.target_repo.repo_id
799 use_rebase = self._use_rebase_for_merging(pull_request)
803 use_rebase = self._use_rebase_for_merging(pull_request)
800 close_branch = self._close_branch_before_merging(pull_request)
804 close_branch = self._close_branch_before_merging(pull_request)
801 user_name = self._user_name_for_merging(pull_request, user)
805 user_name = self._user_name_for_merging(pull_request, user)
802
806
803 target_ref = self._refresh_reference(
807 target_ref = self._refresh_reference(
804 pull_request.target_ref_parts, target_vcs)
808 pull_request.target_ref_parts, target_vcs)
805
809
806 callback_daemon, extras = prepare_callback_daemon(
810 callback_daemon, extras = prepare_callback_daemon(
807 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
811 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
808 host=vcs_settings.HOOKS_HOST,
812 host=vcs_settings.HOOKS_HOST,
809 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
813 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
810
814
811 with callback_daemon:
815 with callback_daemon:
812 # TODO: johbo: Implement a clean way to run a config_override
816 # TODO: johbo: Implement a clean way to run a config_override
813 # for a single call.
817 # for a single call.
814 target_vcs.config.set(
818 target_vcs.config.set(
815 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
819 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
816
820
817 merge_state = target_vcs.merge(
821 merge_state = target_vcs.merge(
818 repo_id, workspace_id, target_ref, source_vcs,
822 repo_id, workspace_id, target_ref, source_vcs,
819 pull_request.source_ref_parts,
823 pull_request.source_ref_parts,
820 user_name=user_name, user_email=user.email,
824 user_name=user_name, user_email=user.email,
821 message=message, use_rebase=use_rebase,
825 message=message, use_rebase=use_rebase,
822 close_branch=close_branch)
826 close_branch=close_branch)
823 return merge_state
827 return merge_state
824
828
825 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
829 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
826 pull_request.merge_rev = merge_state.merge_ref.commit_id
830 pull_request.merge_rev = merge_state.merge_ref.commit_id
827 pull_request.updated_on = datetime.datetime.now()
831 pull_request.updated_on = datetime.datetime.now()
828 close_msg = close_msg or 'Pull request merged and closed'
832 close_msg = close_msg or 'Pull request merged and closed'
829
833
830 CommentsModel().create(
834 CommentsModel().create(
831 text=safe_unicode(close_msg),
835 text=safe_unicode(close_msg),
832 repo=pull_request.target_repo.repo_id,
836 repo=pull_request.target_repo.repo_id,
833 user=user.user_id,
837 user=user.user_id,
834 pull_request=pull_request.pull_request_id,
838 pull_request=pull_request.pull_request_id,
835 f_path=None,
839 f_path=None,
836 line_no=None,
840 line_no=None,
837 closing_pr=True
841 closing_pr=True
838 )
842 )
839
843
840 Session().add(pull_request)
844 Session().add(pull_request)
841 Session().flush()
845 Session().flush()
842 # TODO: paris: replace invalidation with less radical solution
846 # TODO: paris: replace invalidation with less radical solution
843 ScmModel().mark_for_invalidation(
847 ScmModel().mark_for_invalidation(
844 pull_request.target_repo.repo_name)
848 pull_request.target_repo.repo_name)
845 self.trigger_pull_request_hook(pull_request, user, 'merge')
849 self.trigger_pull_request_hook(pull_request, user, 'merge')
846
850
847 def has_valid_update_type(self, pull_request):
851 def has_valid_update_type(self, pull_request):
848 source_ref_type = pull_request.source_ref_parts.type
852 source_ref_type = pull_request.source_ref_parts.type
849 return source_ref_type in self.REF_TYPES
853 return source_ref_type in self.REF_TYPES
850
854
851 def get_flow_commits(self, pull_request):
855 def get_flow_commits(self, pull_request):
852
856
853 # source repo
857 # source repo
854 source_ref_name = pull_request.source_ref_parts.name
858 source_ref_name = pull_request.source_ref_parts.name
855 source_ref_type = pull_request.source_ref_parts.type
859 source_ref_type = pull_request.source_ref_parts.type
856 source_ref_id = pull_request.source_ref_parts.commit_id
860 source_ref_id = pull_request.source_ref_parts.commit_id
857 source_repo = pull_request.source_repo.scm_instance()
861 source_repo = pull_request.source_repo.scm_instance()
858
862
859 try:
863 try:
860 if source_ref_type in self.REF_TYPES:
864 if source_ref_type in self.REF_TYPES:
861 source_commit = source_repo.get_commit(source_ref_name)
865 source_commit = source_repo.get_commit(source_ref_name)
862 else:
866 else:
863 source_commit = source_repo.get_commit(source_ref_id)
867 source_commit = source_repo.get_commit(source_ref_id)
864 except CommitDoesNotExistError:
868 except CommitDoesNotExistError:
865 raise SourceRefMissing()
869 raise SourceRefMissing()
866
870
867 # target repo
871 # target repo
868 target_ref_name = pull_request.target_ref_parts.name
872 target_ref_name = pull_request.target_ref_parts.name
869 target_ref_type = pull_request.target_ref_parts.type
873 target_ref_type = pull_request.target_ref_parts.type
870 target_ref_id = pull_request.target_ref_parts.commit_id
874 target_ref_id = pull_request.target_ref_parts.commit_id
871 target_repo = pull_request.target_repo.scm_instance()
875 target_repo = pull_request.target_repo.scm_instance()
872
876
873 try:
877 try:
874 if target_ref_type in self.REF_TYPES:
878 if target_ref_type in self.REF_TYPES:
875 target_commit = target_repo.get_commit(target_ref_name)
879 target_commit = target_repo.get_commit(target_ref_name)
876 else:
880 else:
877 target_commit = target_repo.get_commit(target_ref_id)
881 target_commit = target_repo.get_commit(target_ref_id)
878 except CommitDoesNotExistError:
882 except CommitDoesNotExistError:
879 raise TargetRefMissing()
883 raise TargetRefMissing()
880
884
881 return source_commit, target_commit
885 return source_commit, target_commit
882
886
883 def update_commits(self, pull_request, updating_user):
887 def update_commits(self, pull_request, updating_user):
884 """
888 """
885 Get the updated list of commits for the pull request
889 Get the updated list of commits for the pull request
886 and return the new pull request version and the list
890 and return the new pull request version and the list
887 of commits processed by this update action
891 of commits processed by this update action
888
892
889 updating_user is the user_object who triggered the update
893 updating_user is the user_object who triggered the update
890 """
894 """
891 pull_request = self.__get_pull_request(pull_request)
895 pull_request = self.__get_pull_request(pull_request)
892 source_ref_type = pull_request.source_ref_parts.type
896 source_ref_type = pull_request.source_ref_parts.type
893 source_ref_name = pull_request.source_ref_parts.name
897 source_ref_name = pull_request.source_ref_parts.name
894 source_ref_id = pull_request.source_ref_parts.commit_id
898 source_ref_id = pull_request.source_ref_parts.commit_id
895
899
896 target_ref_type = pull_request.target_ref_parts.type
900 target_ref_type = pull_request.target_ref_parts.type
897 target_ref_name = pull_request.target_ref_parts.name
901 target_ref_name = pull_request.target_ref_parts.name
898 target_ref_id = pull_request.target_ref_parts.commit_id
902 target_ref_id = pull_request.target_ref_parts.commit_id
899
903
900 if not self.has_valid_update_type(pull_request):
904 if not self.has_valid_update_type(pull_request):
901 log.debug("Skipping update of pull request %s due to ref type: %s",
905 log.debug("Skipping update of pull request %s due to ref type: %s",
902 pull_request, source_ref_type)
906 pull_request, source_ref_type)
903 return UpdateResponse(
907 return UpdateResponse(
904 executed=False,
908 executed=False,
905 reason=UpdateFailureReason.WRONG_REF_TYPE,
909 reason=UpdateFailureReason.WRONG_REF_TYPE,
906 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
910 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
907 source_changed=False, target_changed=False)
911 source_changed=False, target_changed=False)
908
912
909 try:
913 try:
910 source_commit, target_commit = self.get_flow_commits(pull_request)
914 source_commit, target_commit = self.get_flow_commits(pull_request)
911 except SourceRefMissing:
915 except SourceRefMissing:
912 return UpdateResponse(
916 return UpdateResponse(
913 executed=False,
917 executed=False,
914 reason=UpdateFailureReason.MISSING_SOURCE_REF,
918 reason=UpdateFailureReason.MISSING_SOURCE_REF,
915 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
919 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
916 source_changed=False, target_changed=False)
920 source_changed=False, target_changed=False)
917 except TargetRefMissing:
921 except TargetRefMissing:
918 return UpdateResponse(
922 return UpdateResponse(
919 executed=False,
923 executed=False,
920 reason=UpdateFailureReason.MISSING_TARGET_REF,
924 reason=UpdateFailureReason.MISSING_TARGET_REF,
921 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
925 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
922 source_changed=False, target_changed=False)
926 source_changed=False, target_changed=False)
923
927
924 source_changed = source_ref_id != source_commit.raw_id
928 source_changed = source_ref_id != source_commit.raw_id
925 target_changed = target_ref_id != target_commit.raw_id
929 target_changed = target_ref_id != target_commit.raw_id
926
930
927 if not (source_changed or target_changed):
931 if not (source_changed or target_changed):
928 log.debug("Nothing changed in pull request %s", pull_request)
932 log.debug("Nothing changed in pull request %s", pull_request)
929 return UpdateResponse(
933 return UpdateResponse(
930 executed=False,
934 executed=False,
931 reason=UpdateFailureReason.NO_CHANGE,
935 reason=UpdateFailureReason.NO_CHANGE,
932 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
936 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
933 source_changed=target_changed, target_changed=source_changed)
937 source_changed=target_changed, target_changed=source_changed)
934
938
935 change_in_found = 'target repo' if target_changed else 'source repo'
939 change_in_found = 'target repo' if target_changed else 'source repo'
936 log.debug('Updating pull request because of change in %s detected',
940 log.debug('Updating pull request because of change in %s detected',
937 change_in_found)
941 change_in_found)
938
942
939 # Finally there is a need for an update, in case of source change
943 # Finally there is a need for an update, in case of source change
940 # we create a new version, else just an update
944 # we create a new version, else just an update
941 if source_changed:
945 if source_changed:
942 pull_request_version = self._create_version_from_snapshot(pull_request)
946 pull_request_version = self._create_version_from_snapshot(pull_request)
943 self._link_comments_to_version(pull_request_version)
947 self._link_comments_to_version(pull_request_version)
944 else:
948 else:
945 try:
949 try:
946 ver = pull_request.versions[-1]
950 ver = pull_request.versions[-1]
947 except IndexError:
951 except IndexError:
948 ver = None
952 ver = None
949
953
950 pull_request.pull_request_version_id = \
954 pull_request.pull_request_version_id = \
951 ver.pull_request_version_id if ver else None
955 ver.pull_request_version_id if ver else None
952 pull_request_version = pull_request
956 pull_request_version = pull_request
953
957
954 source_repo = pull_request.source_repo.scm_instance()
958 source_repo = pull_request.source_repo.scm_instance()
955 target_repo = pull_request.target_repo.scm_instance()
959 target_repo = pull_request.target_repo.scm_instance()
956
960
957 # re-compute commit ids
961 # re-compute commit ids
958 old_commit_ids = pull_request.revisions
962 old_commit_ids = pull_request.revisions
959 pre_load = ["author", "date", "message", "branch"]
963 pre_load = ["author", "date", "message", "branch"]
960 commit_ranges = target_repo.compare(
964 commit_ranges = target_repo.compare(
961 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
965 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
962 pre_load=pre_load)
966 pre_load=pre_load)
963
967
964 target_ref = target_commit.raw_id
968 target_ref = target_commit.raw_id
965 source_ref = source_commit.raw_id
969 source_ref = source_commit.raw_id
966 ancestor_commit_id = target_repo.get_common_ancestor(
970 ancestor_commit_id = target_repo.get_common_ancestor(
967 target_ref, source_ref, source_repo)
971 target_ref, source_ref, source_repo)
968
972
969 if not ancestor_commit_id:
973 if not ancestor_commit_id:
970 raise ValueError(
974 raise ValueError(
971 'cannot calculate diff info without a common ancestor. '
975 'cannot calculate diff info without a common ancestor. '
972 'Make sure both repositories are related, and have a common forking commit.')
976 'Make sure both repositories are related, and have a common forking commit.')
973
977
974 pull_request.common_ancestor_id = ancestor_commit_id
978 pull_request.common_ancestor_id = ancestor_commit_id
975
979
976 pull_request.source_ref = '%s:%s:%s' % (
980 pull_request.source_ref = '%s:%s:%s' % (
977 source_ref_type, source_ref_name, source_commit.raw_id)
981 source_ref_type, source_ref_name, source_commit.raw_id)
978 pull_request.target_ref = '%s:%s:%s' % (
982 pull_request.target_ref = '%s:%s:%s' % (
979 target_ref_type, target_ref_name, ancestor_commit_id)
983 target_ref_type, target_ref_name, ancestor_commit_id)
980
984
981 pull_request.revisions = [
985 pull_request.revisions = [
982 commit.raw_id for commit in reversed(commit_ranges)]
986 commit.raw_id for commit in reversed(commit_ranges)]
983 pull_request.updated_on = datetime.datetime.now()
987 pull_request.updated_on = datetime.datetime.now()
984 Session().add(pull_request)
988 Session().add(pull_request)
985 new_commit_ids = pull_request.revisions
989 new_commit_ids = pull_request.revisions
986
990
987 old_diff_data, new_diff_data = self._generate_update_diffs(
991 old_diff_data, new_diff_data = self._generate_update_diffs(
988 pull_request, pull_request_version)
992 pull_request, pull_request_version)
989
993
990 # calculate commit and file changes
994 # calculate commit and file changes
991 commit_changes = self._calculate_commit_id_changes(
995 commit_changes = self._calculate_commit_id_changes(
992 old_commit_ids, new_commit_ids)
996 old_commit_ids, new_commit_ids)
993 file_changes = self._calculate_file_changes(
997 file_changes = self._calculate_file_changes(
994 old_diff_data, new_diff_data)
998 old_diff_data, new_diff_data)
995
999
996 # set comments as outdated if DIFFS changed
1000 # set comments as outdated if DIFFS changed
997 CommentsModel().outdate_comments(
1001 CommentsModel().outdate_comments(
998 pull_request, old_diff_data=old_diff_data,
1002 pull_request, old_diff_data=old_diff_data,
999 new_diff_data=new_diff_data)
1003 new_diff_data=new_diff_data)
1000
1004
1001 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1005 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1002 file_node_changes = (
1006 file_node_changes = (
1003 file_changes.added or file_changes.modified or file_changes.removed)
1007 file_changes.added or file_changes.modified or file_changes.removed)
1004 pr_has_changes = valid_commit_changes or file_node_changes
1008 pr_has_changes = valid_commit_changes or file_node_changes
1005
1009
1006 # Add an automatic comment to the pull request, in case
1010 # Add an automatic comment to the pull request, in case
1007 # anything has changed
1011 # anything has changed
1008 if pr_has_changes:
1012 if pr_has_changes:
1009 update_comment = CommentsModel().create(
1013 update_comment = CommentsModel().create(
1010 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1014 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1011 repo=pull_request.target_repo,
1015 repo=pull_request.target_repo,
1012 user=pull_request.author,
1016 user=pull_request.author,
1013 pull_request=pull_request,
1017 pull_request=pull_request,
1014 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1018 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1015
1019
1016 # Update status to "Under Review" for added commits
1020 # Update status to "Under Review" for added commits
1017 for commit_id in commit_changes.added:
1021 for commit_id in commit_changes.added:
1018 ChangesetStatusModel().set_status(
1022 ChangesetStatusModel().set_status(
1019 repo=pull_request.source_repo,
1023 repo=pull_request.source_repo,
1020 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1024 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1021 comment=update_comment,
1025 comment=update_comment,
1022 user=pull_request.author,
1026 user=pull_request.author,
1023 pull_request=pull_request,
1027 pull_request=pull_request,
1024 revision=commit_id)
1028 revision=commit_id)
1025
1029
1026 # send update email to users
1030 # send update email to users
1027 try:
1031 try:
1028 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1032 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1029 ancestor_commit_id=ancestor_commit_id,
1033 ancestor_commit_id=ancestor_commit_id,
1030 commit_changes=commit_changes,
1034 commit_changes=commit_changes,
1031 file_changes=file_changes)
1035 file_changes=file_changes)
1032 except Exception:
1036 except Exception:
1033 log.exception('Failed to send email notification to users')
1037 log.exception('Failed to send email notification to users')
1034
1038
1035 log.debug(
1039 log.debug(
1036 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1040 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1037 'removed_ids: %s', pull_request.pull_request_id,
1041 'removed_ids: %s', pull_request.pull_request_id,
1038 commit_changes.added, commit_changes.common, commit_changes.removed)
1042 commit_changes.added, commit_changes.common, commit_changes.removed)
1039 log.debug(
1043 log.debug(
1040 'Updated pull request with the following file changes: %s',
1044 'Updated pull request with the following file changes: %s',
1041 file_changes)
1045 file_changes)
1042
1046
1043 log.info(
1047 log.info(
1044 "Updated pull request %s from commit %s to commit %s, "
1048 "Updated pull request %s from commit %s to commit %s, "
1045 "stored new version %s of this pull request.",
1049 "stored new version %s of this pull request.",
1046 pull_request.pull_request_id, source_ref_id,
1050 pull_request.pull_request_id, source_ref_id,
1047 pull_request.source_ref_parts.commit_id,
1051 pull_request.source_ref_parts.commit_id,
1048 pull_request_version.pull_request_version_id)
1052 pull_request_version.pull_request_version_id)
1049 Session().commit()
1053 Session().commit()
1050 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1054 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1051
1055
1052 return UpdateResponse(
1056 return UpdateResponse(
1053 executed=True, reason=UpdateFailureReason.NONE,
1057 executed=True, reason=UpdateFailureReason.NONE,
1054 old=pull_request, new=pull_request_version,
1058 old=pull_request, new=pull_request_version,
1055 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1059 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1056 source_changed=source_changed, target_changed=target_changed)
1060 source_changed=source_changed, target_changed=target_changed)
1057
1061
1058 def _create_version_from_snapshot(self, pull_request):
1062 def _create_version_from_snapshot(self, pull_request):
1059 version = PullRequestVersion()
1063 version = PullRequestVersion()
1060 version.title = pull_request.title
1064 version.title = pull_request.title
1061 version.description = pull_request.description
1065 version.description = pull_request.description
1062 version.status = pull_request.status
1066 version.status = pull_request.status
1063 version.pull_request_state = pull_request.pull_request_state
1067 version.pull_request_state = pull_request.pull_request_state
1064 version.created_on = datetime.datetime.now()
1068 version.created_on = datetime.datetime.now()
1065 version.updated_on = pull_request.updated_on
1069 version.updated_on = pull_request.updated_on
1066 version.user_id = pull_request.user_id
1070 version.user_id = pull_request.user_id
1067 version.source_repo = pull_request.source_repo
1071 version.source_repo = pull_request.source_repo
1068 version.source_ref = pull_request.source_ref
1072 version.source_ref = pull_request.source_ref
1069 version.target_repo = pull_request.target_repo
1073 version.target_repo = pull_request.target_repo
1070 version.target_ref = pull_request.target_ref
1074 version.target_ref = pull_request.target_ref
1071
1075
1072 version._last_merge_source_rev = pull_request._last_merge_source_rev
1076 version._last_merge_source_rev = pull_request._last_merge_source_rev
1073 version._last_merge_target_rev = pull_request._last_merge_target_rev
1077 version._last_merge_target_rev = pull_request._last_merge_target_rev
1074 version.last_merge_status = pull_request.last_merge_status
1078 version.last_merge_status = pull_request.last_merge_status
1075 version.last_merge_metadata = pull_request.last_merge_metadata
1079 version.last_merge_metadata = pull_request.last_merge_metadata
1076 version.shadow_merge_ref = pull_request.shadow_merge_ref
1080 version.shadow_merge_ref = pull_request.shadow_merge_ref
1077 version.merge_rev = pull_request.merge_rev
1081 version.merge_rev = pull_request.merge_rev
1078 version.reviewer_data = pull_request.reviewer_data
1082 version.reviewer_data = pull_request.reviewer_data
1079
1083
1080 version.revisions = pull_request.revisions
1084 version.revisions = pull_request.revisions
1081 version.common_ancestor_id = pull_request.common_ancestor_id
1085 version.common_ancestor_id = pull_request.common_ancestor_id
1082 version.pull_request = pull_request
1086 version.pull_request = pull_request
1083 Session().add(version)
1087 Session().add(version)
1084 Session().flush()
1088 Session().flush()
1085
1089
1086 return version
1090 return version
1087
1091
1088 def _generate_update_diffs(self, pull_request, pull_request_version):
1092 def _generate_update_diffs(self, pull_request, pull_request_version):
1089
1093
1090 diff_context = (
1094 diff_context = (
1091 self.DIFF_CONTEXT +
1095 self.DIFF_CONTEXT +
1092 CommentsModel.needed_extra_diff_context())
1096 CommentsModel.needed_extra_diff_context())
1093 hide_whitespace_changes = False
1097 hide_whitespace_changes = False
1094 source_repo = pull_request_version.source_repo
1098 source_repo = pull_request_version.source_repo
1095 source_ref_id = pull_request_version.source_ref_parts.commit_id
1099 source_ref_id = pull_request_version.source_ref_parts.commit_id
1096 target_ref_id = pull_request_version.target_ref_parts.commit_id
1100 target_ref_id = pull_request_version.target_ref_parts.commit_id
1097 old_diff = self._get_diff_from_pr_or_version(
1101 old_diff = self._get_diff_from_pr_or_version(
1098 source_repo, source_ref_id, target_ref_id,
1102 source_repo, source_ref_id, target_ref_id,
1099 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1103 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1100
1104
1101 source_repo = pull_request.source_repo
1105 source_repo = pull_request.source_repo
1102 source_ref_id = pull_request.source_ref_parts.commit_id
1106 source_ref_id = pull_request.source_ref_parts.commit_id
1103 target_ref_id = pull_request.target_ref_parts.commit_id
1107 target_ref_id = pull_request.target_ref_parts.commit_id
1104
1108
1105 new_diff = self._get_diff_from_pr_or_version(
1109 new_diff = self._get_diff_from_pr_or_version(
1106 source_repo, source_ref_id, target_ref_id,
1110 source_repo, source_ref_id, target_ref_id,
1107 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1111 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1108
1112
1109 old_diff_data = diffs.DiffProcessor(old_diff)
1113 old_diff_data = diffs.DiffProcessor(old_diff)
1110 old_diff_data.prepare()
1114 old_diff_data.prepare()
1111 new_diff_data = diffs.DiffProcessor(new_diff)
1115 new_diff_data = diffs.DiffProcessor(new_diff)
1112 new_diff_data.prepare()
1116 new_diff_data.prepare()
1113
1117
1114 return old_diff_data, new_diff_data
1118 return old_diff_data, new_diff_data
1115
1119
1116 def _link_comments_to_version(self, pull_request_version):
1120 def _link_comments_to_version(self, pull_request_version):
1117 """
1121 """
1118 Link all unlinked comments of this pull request to the given version.
1122 Link all unlinked comments of this pull request to the given version.
1119
1123
1120 :param pull_request_version: The `PullRequestVersion` to which
1124 :param pull_request_version: The `PullRequestVersion` to which
1121 the comments shall be linked.
1125 the comments shall be linked.
1122
1126
1123 """
1127 """
1124 pull_request = pull_request_version.pull_request
1128 pull_request = pull_request_version.pull_request
1125 comments = ChangesetComment.query()\
1129 comments = ChangesetComment.query()\
1126 .filter(
1130 .filter(
1127 # TODO: johbo: Should we query for the repo at all here?
1131 # TODO: johbo: Should we query for the repo at all here?
1128 # Pending decision on how comments of PRs are to be related
1132 # Pending decision on how comments of PRs are to be related
1129 # to either the source repo, the target repo or no repo at all.
1133 # to either the source repo, the target repo or no repo at all.
1130 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1134 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1131 ChangesetComment.pull_request == pull_request,
1135 ChangesetComment.pull_request == pull_request,
1132 ChangesetComment.pull_request_version == None)\
1136 ChangesetComment.pull_request_version == None)\
1133 .order_by(ChangesetComment.comment_id.asc())
1137 .order_by(ChangesetComment.comment_id.asc())
1134
1138
1135 # TODO: johbo: Find out why this breaks if it is done in a bulk
1139 # TODO: johbo: Find out why this breaks if it is done in a bulk
1136 # operation.
1140 # operation.
1137 for comment in comments:
1141 for comment in comments:
1138 comment.pull_request_version_id = (
1142 comment.pull_request_version_id = (
1139 pull_request_version.pull_request_version_id)
1143 pull_request_version.pull_request_version_id)
1140 Session().add(comment)
1144 Session().add(comment)
1141
1145
1142 def _calculate_commit_id_changes(self, old_ids, new_ids):
1146 def _calculate_commit_id_changes(self, old_ids, new_ids):
1143 added = [x for x in new_ids if x not in old_ids]
1147 added = [x for x in new_ids if x not in old_ids]
1144 common = [x for x in new_ids if x in old_ids]
1148 common = [x for x in new_ids if x in old_ids]
1145 removed = [x for x in old_ids if x not in new_ids]
1149 removed = [x for x in old_ids if x not in new_ids]
1146 total = new_ids
1150 total = new_ids
1147 return ChangeTuple(added, common, removed, total)
1151 return ChangeTuple(added, common, removed, total)
1148
1152
1149 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1153 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1150
1154
1151 old_files = OrderedDict()
1155 old_files = OrderedDict()
1152 for diff_data in old_diff_data.parsed_diff:
1156 for diff_data in old_diff_data.parsed_diff:
1153 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1157 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1154
1158
1155 added_files = []
1159 added_files = []
1156 modified_files = []
1160 modified_files = []
1157 removed_files = []
1161 removed_files = []
1158 for diff_data in new_diff_data.parsed_diff:
1162 for diff_data in new_diff_data.parsed_diff:
1159 new_filename = diff_data['filename']
1163 new_filename = diff_data['filename']
1160 new_hash = md5_safe(diff_data['raw_diff'])
1164 new_hash = md5_safe(diff_data['raw_diff'])
1161
1165
1162 old_hash = old_files.get(new_filename)
1166 old_hash = old_files.get(new_filename)
1163 if not old_hash:
1167 if not old_hash:
1164 # file is not present in old diff, we have to figure out from parsed diff
1168 # file is not present in old diff, we have to figure out from parsed diff
1165 # operation ADD/REMOVE
1169 # operation ADD/REMOVE
1166 operations_dict = diff_data['stats']['ops']
1170 operations_dict = diff_data['stats']['ops']
1167 if diffs.DEL_FILENODE in operations_dict:
1171 if diffs.DEL_FILENODE in operations_dict:
1168 removed_files.append(new_filename)
1172 removed_files.append(new_filename)
1169 else:
1173 else:
1170 added_files.append(new_filename)
1174 added_files.append(new_filename)
1171 else:
1175 else:
1172 if new_hash != old_hash:
1176 if new_hash != old_hash:
1173 modified_files.append(new_filename)
1177 modified_files.append(new_filename)
1174 # now remove a file from old, since we have seen it already
1178 # now remove a file from old, since we have seen it already
1175 del old_files[new_filename]
1179 del old_files[new_filename]
1176
1180
1177 # removed files is when there are present in old, but not in NEW,
1181 # removed files is when there are present in old, but not in NEW,
1178 # since we remove old files that are present in new diff, left-overs
1182 # since we remove old files that are present in new diff, left-overs
1179 # if any should be the removed files
1183 # if any should be the removed files
1180 removed_files.extend(old_files.keys())
1184 removed_files.extend(old_files.keys())
1181
1185
1182 return FileChangeTuple(added_files, modified_files, removed_files)
1186 return FileChangeTuple(added_files, modified_files, removed_files)
1183
1187
1184 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1188 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1185 """
1189 """
1186 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1190 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1187 so it's always looking the same disregarding on which default
1191 so it's always looking the same disregarding on which default
1188 renderer system is using.
1192 renderer system is using.
1189
1193
1190 :param ancestor_commit_id: ancestor raw_id
1194 :param ancestor_commit_id: ancestor raw_id
1191 :param changes: changes named tuple
1195 :param changes: changes named tuple
1192 :param file_changes: file changes named tuple
1196 :param file_changes: file changes named tuple
1193
1197
1194 """
1198 """
1195 new_status = ChangesetStatus.get_status_lbl(
1199 new_status = ChangesetStatus.get_status_lbl(
1196 ChangesetStatus.STATUS_UNDER_REVIEW)
1200 ChangesetStatus.STATUS_UNDER_REVIEW)
1197
1201
1198 changed_files = (
1202 changed_files = (
1199 file_changes.added + file_changes.modified + file_changes.removed)
1203 file_changes.added + file_changes.modified + file_changes.removed)
1200
1204
1201 params = {
1205 params = {
1202 'under_review_label': new_status,
1206 'under_review_label': new_status,
1203 'added_commits': changes.added,
1207 'added_commits': changes.added,
1204 'removed_commits': changes.removed,
1208 'removed_commits': changes.removed,
1205 'changed_files': changed_files,
1209 'changed_files': changed_files,
1206 'added_files': file_changes.added,
1210 'added_files': file_changes.added,
1207 'modified_files': file_changes.modified,
1211 'modified_files': file_changes.modified,
1208 'removed_files': file_changes.removed,
1212 'removed_files': file_changes.removed,
1209 'ancestor_commit_id': ancestor_commit_id
1213 'ancestor_commit_id': ancestor_commit_id
1210 }
1214 }
1211 renderer = RstTemplateRenderer()
1215 renderer = RstTemplateRenderer()
1212 return renderer.render('pull_request_update.mako', **params)
1216 return renderer.render('pull_request_update.mako', **params)
1213
1217
1214 def edit(self, pull_request, title, description, description_renderer, user):
1218 def edit(self, pull_request, title, description, description_renderer, user):
1215 pull_request = self.__get_pull_request(pull_request)
1219 pull_request = self.__get_pull_request(pull_request)
1216 old_data = pull_request.get_api_data(with_merge_state=False)
1220 old_data = pull_request.get_api_data(with_merge_state=False)
1217 if pull_request.is_closed():
1221 if pull_request.is_closed():
1218 raise ValueError('This pull request is closed')
1222 raise ValueError('This pull request is closed')
1219 if title:
1223 if title:
1220 pull_request.title = title
1224 pull_request.title = title
1221 pull_request.description = description
1225 pull_request.description = description
1222 pull_request.updated_on = datetime.datetime.now()
1226 pull_request.updated_on = datetime.datetime.now()
1223 pull_request.description_renderer = description_renderer
1227 pull_request.description_renderer = description_renderer
1224 Session().add(pull_request)
1228 Session().add(pull_request)
1225 self._log_audit_action(
1229 self._log_audit_action(
1226 'repo.pull_request.edit', {'old_data': old_data},
1230 'repo.pull_request.edit', {'old_data': old_data},
1227 user, pull_request)
1231 user, pull_request)
1228
1232
1229 def update_reviewers(self, pull_request, reviewer_data, user):
1233 def update_reviewers(self, pull_request, reviewer_data, user):
1230 """
1234 """
1231 Update the reviewers in the pull request
1235 Update the reviewers in the pull request
1232
1236
1233 :param pull_request: the pr to update
1237 :param pull_request: the pr to update
1234 :param reviewer_data: list of tuples
1238 :param reviewer_data: list of tuples
1235 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1239 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1236 :param user: current use who triggers this action
1240 :param user: current use who triggers this action
1237 """
1241 """
1238
1242
1239 pull_request = self.__get_pull_request(pull_request)
1243 pull_request = self.__get_pull_request(pull_request)
1240 if pull_request.is_closed():
1244 if pull_request.is_closed():
1241 raise ValueError('This pull request is closed')
1245 raise ValueError('This pull request is closed')
1242
1246
1243 reviewers = {}
1247 reviewers = {}
1244 for user_id, reasons, mandatory, role, rules in reviewer_data:
1248 for user_id, reasons, mandatory, role, rules in reviewer_data:
1245 if isinstance(user_id, (int, compat.string_types)):
1249 if isinstance(user_id, (int, compat.string_types)):
1246 user_id = self._get_user(user_id).user_id
1250 user_id = self._get_user(user_id).user_id
1247 reviewers[user_id] = {
1251 reviewers[user_id] = {
1248 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1252 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1249
1253
1250 reviewers_ids = set(reviewers.keys())
1254 reviewers_ids = set(reviewers.keys())
1251 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1255 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1252 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1256 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1253
1257
1254 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1258 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1255
1259
1256 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1260 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1257 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1261 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1258
1262
1259 log.debug("Adding %s reviewers", ids_to_add)
1263 log.debug("Adding %s reviewers", ids_to_add)
1260 log.debug("Removing %s reviewers", ids_to_remove)
1264 log.debug("Removing %s reviewers", ids_to_remove)
1261 changed = False
1265 changed = False
1262 added_audit_reviewers = []
1266 added_audit_reviewers = []
1263 removed_audit_reviewers = []
1267 removed_audit_reviewers = []
1264
1268
1265 for uid in ids_to_add:
1269 for uid in ids_to_add:
1266 changed = True
1270 changed = True
1267 _usr = self._get_user(uid)
1271 _usr = self._get_user(uid)
1268 reviewer = PullRequestReviewers()
1272 reviewer = PullRequestReviewers()
1269 reviewer.user = _usr
1273 reviewer.user = _usr
1270 reviewer.pull_request = pull_request
1274 reviewer.pull_request = pull_request
1271 reviewer.reasons = reviewers[uid]['reasons']
1275 reviewer.reasons = reviewers[uid]['reasons']
1272 # NOTE(marcink): mandatory shouldn't be changed now
1276 # NOTE(marcink): mandatory shouldn't be changed now
1273 # reviewer.mandatory = reviewers[uid]['reasons']
1277 # reviewer.mandatory = reviewers[uid]['reasons']
1274 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1278 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1275 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1279 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1276 Session().add(reviewer)
1280 Session().add(reviewer)
1277 added_audit_reviewers.append(reviewer.get_dict())
1281 added_audit_reviewers.append(reviewer.get_dict())
1278
1282
1279 for uid in ids_to_remove:
1283 for uid in ids_to_remove:
1280 changed = True
1284 changed = True
1281 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1285 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1282 # This is an edge case that handles previous state of having the same reviewer twice.
1286 # This is an edge case that handles previous state of having the same reviewer twice.
1283 # this CAN happen due to the lack of DB checks
1287 # this CAN happen due to the lack of DB checks
1284 reviewers = PullRequestReviewers.query()\
1288 reviewers = PullRequestReviewers.query()\
1285 .filter(PullRequestReviewers.user_id == uid,
1289 .filter(PullRequestReviewers.user_id == uid,
1286 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1290 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1287 PullRequestReviewers.pull_request == pull_request)\
1291 PullRequestReviewers.pull_request == pull_request)\
1288 .all()
1292 .all()
1289
1293
1290 for obj in reviewers:
1294 for obj in reviewers:
1291 added_audit_reviewers.append(obj.get_dict())
1295 added_audit_reviewers.append(obj.get_dict())
1292 Session().delete(obj)
1296 Session().delete(obj)
1293
1297
1294 if changed:
1298 if changed:
1295 Session().expire_all()
1299 Session().expire_all()
1296 pull_request.updated_on = datetime.datetime.now()
1300 pull_request.updated_on = datetime.datetime.now()
1297 Session().add(pull_request)
1301 Session().add(pull_request)
1298
1302
1299 # finally store audit logs
1303 # finally store audit logs
1300 for user_data in added_audit_reviewers:
1304 for user_data in added_audit_reviewers:
1301 self._log_audit_action(
1305 self._log_audit_action(
1302 'repo.pull_request.reviewer.add', {'data': user_data},
1306 'repo.pull_request.reviewer.add', {'data': user_data},
1303 user, pull_request)
1307 user, pull_request)
1304 for user_data in removed_audit_reviewers:
1308 for user_data in removed_audit_reviewers:
1305 self._log_audit_action(
1309 self._log_audit_action(
1306 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1310 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1307 user, pull_request)
1311 user, pull_request)
1308
1312
1309 self.notify_reviewers(pull_request, ids_to_add, user.get_instance())
1313 self.notify_reviewers(pull_request, ids_to_add, user.get_instance())
1310 return ids_to_add, ids_to_remove
1314 return ids_to_add, ids_to_remove
1311
1315
1312 def update_observers(self, pull_request, observer_data, user):
1316 def update_observers(self, pull_request, observer_data, user):
1313 """
1317 """
1314 Update the observers in the pull request
1318 Update the observers in the pull request
1315
1319
1316 :param pull_request: the pr to update
1320 :param pull_request: the pr to update
1317 :param observer_data: list of tuples
1321 :param observer_data: list of tuples
1318 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1322 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1319 :param user: current use who triggers this action
1323 :param user: current use who triggers this action
1320 """
1324 """
1321 pull_request = self.__get_pull_request(pull_request)
1325 pull_request = self.__get_pull_request(pull_request)
1322 if pull_request.is_closed():
1326 if pull_request.is_closed():
1323 raise ValueError('This pull request is closed')
1327 raise ValueError('This pull request is closed')
1324
1328
1325 observers = {}
1329 observers = {}
1326 for user_id, reasons, mandatory, role, rules in observer_data:
1330 for user_id, reasons, mandatory, role, rules in observer_data:
1327 if isinstance(user_id, (int, compat.string_types)):
1331 if isinstance(user_id, (int, compat.string_types)):
1328 user_id = self._get_user(user_id).user_id
1332 user_id = self._get_user(user_id).user_id
1329 observers[user_id] = {
1333 observers[user_id] = {
1330 'reasons': reasons, 'observers': mandatory, 'role': role}
1334 'reasons': reasons, 'observers': mandatory, 'role': role}
1331
1335
1332 observers_ids = set(observers.keys())
1336 observers_ids = set(observers.keys())
1333 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1337 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1334 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1338 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1335
1339
1336 current_observers_ids = set([x.user.user_id for x in current_observers])
1340 current_observers_ids = set([x.user.user_id for x in current_observers])
1337
1341
1338 ids_to_add = observers_ids.difference(current_observers_ids)
1342 ids_to_add = observers_ids.difference(current_observers_ids)
1339 ids_to_remove = current_observers_ids.difference(observers_ids)
1343 ids_to_remove = current_observers_ids.difference(observers_ids)
1340
1344
1341 log.debug("Adding %s observer", ids_to_add)
1345 log.debug("Adding %s observer", ids_to_add)
1342 log.debug("Removing %s observer", ids_to_remove)
1346 log.debug("Removing %s observer", ids_to_remove)
1343 changed = False
1347 changed = False
1344 added_audit_observers = []
1348 added_audit_observers = []
1345 removed_audit_observers = []
1349 removed_audit_observers = []
1346
1350
1347 for uid in ids_to_add:
1351 for uid in ids_to_add:
1348 changed = True
1352 changed = True
1349 _usr = self._get_user(uid)
1353 _usr = self._get_user(uid)
1350 observer = PullRequestReviewers()
1354 observer = PullRequestReviewers()
1351 observer.user = _usr
1355 observer.user = _usr
1352 observer.pull_request = pull_request
1356 observer.pull_request = pull_request
1353 observer.reasons = observers[uid]['reasons']
1357 observer.reasons = observers[uid]['reasons']
1354 # NOTE(marcink): mandatory shouldn't be changed now
1358 # NOTE(marcink): mandatory shouldn't be changed now
1355 # observer.mandatory = observer[uid]['reasons']
1359 # observer.mandatory = observer[uid]['reasons']
1356
1360
1357 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1361 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1358 observer.role = PullRequestReviewers.ROLE_OBSERVER
1362 observer.role = PullRequestReviewers.ROLE_OBSERVER
1359 Session().add(observer)
1363 Session().add(observer)
1360 added_audit_observers.append(observer.get_dict())
1364 added_audit_observers.append(observer.get_dict())
1361
1365
1362 for uid in ids_to_remove:
1366 for uid in ids_to_remove:
1363 changed = True
1367 changed = True
1364 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1368 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1365 # This is an edge case that handles previous state of having the same reviewer twice.
1369 # This is an edge case that handles previous state of having the same reviewer twice.
1366 # this CAN happen due to the lack of DB checks
1370 # this CAN happen due to the lack of DB checks
1367 observers = PullRequestReviewers.query()\
1371 observers = PullRequestReviewers.query()\
1368 .filter(PullRequestReviewers.user_id == uid,
1372 .filter(PullRequestReviewers.user_id == uid,
1369 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1373 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1370 PullRequestReviewers.pull_request == pull_request)\
1374 PullRequestReviewers.pull_request == pull_request)\
1371 .all()
1375 .all()
1372
1376
1373 for obj in observers:
1377 for obj in observers:
1374 added_audit_observers.append(obj.get_dict())
1378 added_audit_observers.append(obj.get_dict())
1375 Session().delete(obj)
1379 Session().delete(obj)
1376
1380
1377 if changed:
1381 if changed:
1378 Session().expire_all()
1382 Session().expire_all()
1379 pull_request.updated_on = datetime.datetime.now()
1383 pull_request.updated_on = datetime.datetime.now()
1380 Session().add(pull_request)
1384 Session().add(pull_request)
1381
1385
1382 # finally store audit logs
1386 # finally store audit logs
1383 for user_data in added_audit_observers:
1387 for user_data in added_audit_observers:
1384 self._log_audit_action(
1388 self._log_audit_action(
1385 'repo.pull_request.observer.add', {'data': user_data},
1389 'repo.pull_request.observer.add', {'data': user_data},
1386 user, pull_request)
1390 user, pull_request)
1387 for user_data in removed_audit_observers:
1391 for user_data in removed_audit_observers:
1388 self._log_audit_action(
1392 self._log_audit_action(
1389 'repo.pull_request.observer.delete', {'old_data': user_data},
1393 'repo.pull_request.observer.delete', {'old_data': user_data},
1390 user, pull_request)
1394 user, pull_request)
1391
1395
1392 self.notify_observers(pull_request, ids_to_add, user.get_instance())
1396 self.notify_observers(pull_request, ids_to_add, user.get_instance())
1393 return ids_to_add, ids_to_remove
1397 return ids_to_add, ids_to_remove
1394
1398
1395 def get_url(self, pull_request, request=None, permalink=False):
1399 def get_url(self, pull_request, request=None, permalink=False):
1396 if not request:
1400 if not request:
1397 request = get_current_request()
1401 request = get_current_request()
1398
1402
1399 if permalink:
1403 if permalink:
1400 return request.route_url(
1404 return request.route_url(
1401 'pull_requests_global',
1405 'pull_requests_global',
1402 pull_request_id=pull_request.pull_request_id,)
1406 pull_request_id=pull_request.pull_request_id,)
1403 else:
1407 else:
1404 return request.route_url('pullrequest_show',
1408 return request.route_url('pullrequest_show',
1405 repo_name=safe_str(pull_request.target_repo.repo_name),
1409 repo_name=safe_str(pull_request.target_repo.repo_name),
1406 pull_request_id=pull_request.pull_request_id,)
1410 pull_request_id=pull_request.pull_request_id,)
1407
1411
1408 def get_shadow_clone_url(self, pull_request, request=None):
1412 def get_shadow_clone_url(self, pull_request, request=None):
1409 """
1413 """
1410 Returns qualified url pointing to the shadow repository. If this pull
1414 Returns qualified url pointing to the shadow repository. If this pull
1411 request is closed there is no shadow repository and ``None`` will be
1415 request is closed there is no shadow repository and ``None`` will be
1412 returned.
1416 returned.
1413 """
1417 """
1414 if pull_request.is_closed():
1418 if pull_request.is_closed():
1415 return None
1419 return None
1416 else:
1420 else:
1417 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1421 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1418 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1422 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1419
1423
1420 def _notify_reviewers(self, pull_request, user_ids, role, user):
1424 def _notify_reviewers(self, pull_request, user_ids, role, user):
1421 # notification to reviewers/observers
1425 # notification to reviewers/observers
1422 if not user_ids:
1426 if not user_ids:
1423 return
1427 return
1424
1428
1425 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1429 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1426
1430
1427 pull_request_obj = pull_request
1431 pull_request_obj = pull_request
1428 # get the current participants of this pull request
1432 # get the current participants of this pull request
1429 recipients = user_ids
1433 recipients = user_ids
1430 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1434 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1431
1435
1432 pr_source_repo = pull_request_obj.source_repo
1436 pr_source_repo = pull_request_obj.source_repo
1433 pr_target_repo = pull_request_obj.target_repo
1437 pr_target_repo = pull_request_obj.target_repo
1434
1438
1435 pr_url = h.route_url('pullrequest_show',
1439 pr_url = h.route_url('pullrequest_show',
1436 repo_name=pr_target_repo.repo_name,
1440 repo_name=pr_target_repo.repo_name,
1437 pull_request_id=pull_request_obj.pull_request_id,)
1441 pull_request_id=pull_request_obj.pull_request_id,)
1438
1442
1439 # set some variables for email notification
1443 # set some variables for email notification
1440 pr_target_repo_url = h.route_url(
1444 pr_target_repo_url = h.route_url(
1441 'repo_summary', repo_name=pr_target_repo.repo_name)
1445 'repo_summary', repo_name=pr_target_repo.repo_name)
1442
1446
1443 pr_source_repo_url = h.route_url(
1447 pr_source_repo_url = h.route_url(
1444 'repo_summary', repo_name=pr_source_repo.repo_name)
1448 'repo_summary', repo_name=pr_source_repo.repo_name)
1445
1449
1446 # pull request specifics
1450 # pull request specifics
1447 pull_request_commits = [
1451 pull_request_commits = [
1448 (x.raw_id, x.message)
1452 (x.raw_id, x.message)
1449 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1453 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1450
1454
1451 current_rhodecode_user = user
1455 current_rhodecode_user = user
1452 kwargs = {
1456 kwargs = {
1453 'user': current_rhodecode_user,
1457 'user': current_rhodecode_user,
1454 'pull_request_author': pull_request.author,
1458 'pull_request_author': pull_request.author,
1455 'pull_request': pull_request_obj,
1459 'pull_request': pull_request_obj,
1456 'pull_request_commits': pull_request_commits,
1460 'pull_request_commits': pull_request_commits,
1457
1461
1458 'pull_request_target_repo': pr_target_repo,
1462 'pull_request_target_repo': pr_target_repo,
1459 'pull_request_target_repo_url': pr_target_repo_url,
1463 'pull_request_target_repo_url': pr_target_repo_url,
1460
1464
1461 'pull_request_source_repo': pr_source_repo,
1465 'pull_request_source_repo': pr_source_repo,
1462 'pull_request_source_repo_url': pr_source_repo_url,
1466 'pull_request_source_repo_url': pr_source_repo_url,
1463
1467
1464 'pull_request_url': pr_url,
1468 'pull_request_url': pr_url,
1465 'thread_ids': [pr_url],
1469 'thread_ids': [pr_url],
1466 'user_role': role
1470 'user_role': role
1467 }
1471 }
1468
1472
1469 # pre-generate the subject for notification itself
1473 # pre-generate the subject for notification itself
1470 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1474 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1471 notification_type, **kwargs)
1475 notification_type, **kwargs)
1472
1476
1473 # create notification objects, and emails
1477 # create notification objects, and emails
1474 NotificationModel().create(
1478 NotificationModel().create(
1475 created_by=current_rhodecode_user,
1479 created_by=current_rhodecode_user,
1476 notification_subject=subject,
1480 notification_subject=subject,
1477 notification_body=body_plaintext,
1481 notification_body=body_plaintext,
1478 notification_type=notification_type,
1482 notification_type=notification_type,
1479 recipients=recipients,
1483 recipients=recipients,
1480 email_kwargs=kwargs,
1484 email_kwargs=kwargs,
1481 )
1485 )
1482
1486
1483 def notify_reviewers(self, pull_request, reviewers_ids, user):
1487 def notify_reviewers(self, pull_request, reviewers_ids, user):
1484 return self._notify_reviewers(pull_request, reviewers_ids,
1488 return self._notify_reviewers(pull_request, reviewers_ids,
1485 PullRequestReviewers.ROLE_REVIEWER, user)
1489 PullRequestReviewers.ROLE_REVIEWER, user)
1486
1490
1487 def notify_observers(self, pull_request, observers_ids, user):
1491 def notify_observers(self, pull_request, observers_ids, user):
1488 return self._notify_reviewers(pull_request, observers_ids,
1492 return self._notify_reviewers(pull_request, observers_ids,
1489 PullRequestReviewers.ROLE_OBSERVER, user)
1493 PullRequestReviewers.ROLE_OBSERVER, user)
1490
1494
1491 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1495 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1492 commit_changes, file_changes):
1496 commit_changes, file_changes):
1493
1497
1494 updating_user_id = updating_user.user_id
1498 updating_user_id = updating_user.user_id
1495 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1499 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1496 # NOTE(marcink): send notification to all other users except to
1500 # NOTE(marcink): send notification to all other users except to
1497 # person who updated the PR
1501 # person who updated the PR
1498 recipients = reviewers.difference(set([updating_user_id]))
1502 recipients = reviewers.difference(set([updating_user_id]))
1499
1503
1500 log.debug('Notify following recipients about pull-request update %s', recipients)
1504 log.debug('Notify following recipients about pull-request update %s', recipients)
1501
1505
1502 pull_request_obj = pull_request
1506 pull_request_obj = pull_request
1503
1507
1504 # send email about the update
1508 # send email about the update
1505 changed_files = (
1509 changed_files = (
1506 file_changes.added + file_changes.modified + file_changes.removed)
1510 file_changes.added + file_changes.modified + file_changes.removed)
1507
1511
1508 pr_source_repo = pull_request_obj.source_repo
1512 pr_source_repo = pull_request_obj.source_repo
1509 pr_target_repo = pull_request_obj.target_repo
1513 pr_target_repo = pull_request_obj.target_repo
1510
1514
1511 pr_url = h.route_url('pullrequest_show',
1515 pr_url = h.route_url('pullrequest_show',
1512 repo_name=pr_target_repo.repo_name,
1516 repo_name=pr_target_repo.repo_name,
1513 pull_request_id=pull_request_obj.pull_request_id,)
1517 pull_request_id=pull_request_obj.pull_request_id,)
1514
1518
1515 # set some variables for email notification
1519 # set some variables for email notification
1516 pr_target_repo_url = h.route_url(
1520 pr_target_repo_url = h.route_url(
1517 'repo_summary', repo_name=pr_target_repo.repo_name)
1521 'repo_summary', repo_name=pr_target_repo.repo_name)
1518
1522
1519 pr_source_repo_url = h.route_url(
1523 pr_source_repo_url = h.route_url(
1520 'repo_summary', repo_name=pr_source_repo.repo_name)
1524 'repo_summary', repo_name=pr_source_repo.repo_name)
1521
1525
1522 email_kwargs = {
1526 email_kwargs = {
1523 'date': datetime.datetime.now(),
1527 'date': datetime.datetime.now(),
1524 'updating_user': updating_user,
1528 'updating_user': updating_user,
1525
1529
1526 'pull_request': pull_request_obj,
1530 'pull_request': pull_request_obj,
1527
1531
1528 'pull_request_target_repo': pr_target_repo,
1532 'pull_request_target_repo': pr_target_repo,
1529 'pull_request_target_repo_url': pr_target_repo_url,
1533 'pull_request_target_repo_url': pr_target_repo_url,
1530
1534
1531 'pull_request_source_repo': pr_source_repo,
1535 'pull_request_source_repo': pr_source_repo,
1532 'pull_request_source_repo_url': pr_source_repo_url,
1536 'pull_request_source_repo_url': pr_source_repo_url,
1533
1537
1534 'pull_request_url': pr_url,
1538 'pull_request_url': pr_url,
1535
1539
1536 'ancestor_commit_id': ancestor_commit_id,
1540 'ancestor_commit_id': ancestor_commit_id,
1537 'added_commits': commit_changes.added,
1541 'added_commits': commit_changes.added,
1538 'removed_commits': commit_changes.removed,
1542 'removed_commits': commit_changes.removed,
1539 'changed_files': changed_files,
1543 'changed_files': changed_files,
1540 'added_files': file_changes.added,
1544 'added_files': file_changes.added,
1541 'modified_files': file_changes.modified,
1545 'modified_files': file_changes.modified,
1542 'removed_files': file_changes.removed,
1546 'removed_files': file_changes.removed,
1543 'thread_ids': [pr_url],
1547 'thread_ids': [pr_url],
1544 }
1548 }
1545
1549
1546 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1550 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1547 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1551 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1548
1552
1549 # create notification objects, and emails
1553 # create notification objects, and emails
1550 NotificationModel().create(
1554 NotificationModel().create(
1551 created_by=updating_user,
1555 created_by=updating_user,
1552 notification_subject=subject,
1556 notification_subject=subject,
1553 notification_body=body_plaintext,
1557 notification_body=body_plaintext,
1554 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1558 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1555 recipients=recipients,
1559 recipients=recipients,
1556 email_kwargs=email_kwargs,
1560 email_kwargs=email_kwargs,
1557 )
1561 )
1558
1562
1559 def delete(self, pull_request, user=None):
1563 def delete(self, pull_request, user=None):
1560 if not user:
1564 if not user:
1561 user = getattr(get_current_rhodecode_user(), 'username', None)
1565 user = getattr(get_current_rhodecode_user(), 'username', None)
1562
1566
1563 pull_request = self.__get_pull_request(pull_request)
1567 pull_request = self.__get_pull_request(pull_request)
1564 old_data = pull_request.get_api_data(with_merge_state=False)
1568 old_data = pull_request.get_api_data(with_merge_state=False)
1565 self._cleanup_merge_workspace(pull_request)
1569 self._cleanup_merge_workspace(pull_request)
1566 self._log_audit_action(
1570 self._log_audit_action(
1567 'repo.pull_request.delete', {'old_data': old_data},
1571 'repo.pull_request.delete', {'old_data': old_data},
1568 user, pull_request)
1572 user, pull_request)
1569 Session().delete(pull_request)
1573 Session().delete(pull_request)
1570
1574
1571 def close_pull_request(self, pull_request, user):
1575 def close_pull_request(self, pull_request, user):
1572 pull_request = self.__get_pull_request(pull_request)
1576 pull_request = self.__get_pull_request(pull_request)
1573 self._cleanup_merge_workspace(pull_request)
1577 self._cleanup_merge_workspace(pull_request)
1574 pull_request.status = PullRequest.STATUS_CLOSED
1578 pull_request.status = PullRequest.STATUS_CLOSED
1575 pull_request.updated_on = datetime.datetime.now()
1579 pull_request.updated_on = datetime.datetime.now()
1576 Session().add(pull_request)
1580 Session().add(pull_request)
1577 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1581 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1578
1582
1579 pr_data = pull_request.get_api_data(with_merge_state=False)
1583 pr_data = pull_request.get_api_data(with_merge_state=False)
1580 self._log_audit_action(
1584 self._log_audit_action(
1581 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1585 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1582
1586
1583 def close_pull_request_with_comment(
1587 def close_pull_request_with_comment(
1584 self, pull_request, user, repo, message=None, auth_user=None):
1588 self, pull_request, user, repo, message=None, auth_user=None):
1585
1589
1586 pull_request_review_status = pull_request.calculated_review_status()
1590 pull_request_review_status = pull_request.calculated_review_status()
1587
1591
1588 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1592 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1589 # approved only if we have voting consent
1593 # approved only if we have voting consent
1590 status = ChangesetStatus.STATUS_APPROVED
1594 status = ChangesetStatus.STATUS_APPROVED
1591 else:
1595 else:
1592 status = ChangesetStatus.STATUS_REJECTED
1596 status = ChangesetStatus.STATUS_REJECTED
1593 status_lbl = ChangesetStatus.get_status_lbl(status)
1597 status_lbl = ChangesetStatus.get_status_lbl(status)
1594
1598
1595 default_message = (
1599 default_message = (
1596 'Closing with status change {transition_icon} {status}.'
1600 'Closing with status change {transition_icon} {status}.'
1597 ).format(transition_icon='>', status=status_lbl)
1601 ).format(transition_icon='>', status=status_lbl)
1598 text = message or default_message
1602 text = message or default_message
1599
1603
1600 # create a comment, and link it to new status
1604 # create a comment, and link it to new status
1601 comment = CommentsModel().create(
1605 comment = CommentsModel().create(
1602 text=text,
1606 text=text,
1603 repo=repo.repo_id,
1607 repo=repo.repo_id,
1604 user=user.user_id,
1608 user=user.user_id,
1605 pull_request=pull_request.pull_request_id,
1609 pull_request=pull_request.pull_request_id,
1606 status_change=status_lbl,
1610 status_change=status_lbl,
1607 status_change_type=status,
1611 status_change_type=status,
1608 closing_pr=True,
1612 closing_pr=True,
1609 auth_user=auth_user,
1613 auth_user=auth_user,
1610 )
1614 )
1611
1615
1612 # calculate old status before we change it
1616 # calculate old status before we change it
1613 old_calculated_status = pull_request.calculated_review_status()
1617 old_calculated_status = pull_request.calculated_review_status()
1614 ChangesetStatusModel().set_status(
1618 ChangesetStatusModel().set_status(
1615 repo.repo_id,
1619 repo.repo_id,
1616 status,
1620 status,
1617 user.user_id,
1621 user.user_id,
1618 comment=comment,
1622 comment=comment,
1619 pull_request=pull_request.pull_request_id
1623 pull_request=pull_request.pull_request_id
1620 )
1624 )
1621
1625
1622 Session().flush()
1626 Session().flush()
1623
1627
1624 self.trigger_pull_request_hook(pull_request, user, 'comment',
1628 self.trigger_pull_request_hook(pull_request, user, 'comment',
1625 data={'comment': comment})
1629 data={'comment': comment})
1626
1630
1627 # we now calculate the status of pull request again, and based on that
1631 # we now calculate the status of pull request again, and based on that
1628 # calculation trigger status change. This might happen in cases
1632 # calculation trigger status change. This might happen in cases
1629 # that non-reviewer admin closes a pr, which means his vote doesn't
1633 # that non-reviewer admin closes a pr, which means his vote doesn't
1630 # change the status, while if he's a reviewer this might change it.
1634 # change the status, while if he's a reviewer this might change it.
1631 calculated_status = pull_request.calculated_review_status()
1635 calculated_status = pull_request.calculated_review_status()
1632 if old_calculated_status != calculated_status:
1636 if old_calculated_status != calculated_status:
1633 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1637 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1634 data={'status': calculated_status})
1638 data={'status': calculated_status})
1635
1639
1636 # finally close the PR
1640 # finally close the PR
1637 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1641 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1638
1642
1639 return comment, status
1643 return comment, status
1640
1644
1641 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1645 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1642 _ = translator or get_current_request().translate
1646 _ = translator or get_current_request().translate
1643
1647
1644 if not self._is_merge_enabled(pull_request):
1648 if not self._is_merge_enabled(pull_request):
1645 return None, False, _('Server-side pull request merging is disabled.')
1649 return None, False, _('Server-side pull request merging is disabled.')
1646
1650
1647 if pull_request.is_closed():
1651 if pull_request.is_closed():
1648 return None, False, _('This pull request is closed.')
1652 return None, False, _('This pull request is closed.')
1649
1653
1650 merge_possible, msg = self._check_repo_requirements(
1654 merge_possible, msg = self._check_repo_requirements(
1651 target=pull_request.target_repo, source=pull_request.source_repo,
1655 target=pull_request.target_repo, source=pull_request.source_repo,
1652 translator=_)
1656 translator=_)
1653 if not merge_possible:
1657 if not merge_possible:
1654 return None, merge_possible, msg
1658 return None, merge_possible, msg
1655
1659
1656 try:
1660 try:
1657 merge_response = self._try_merge(
1661 merge_response = self._try_merge(
1658 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1662 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1659 log.debug("Merge response: %s", merge_response)
1663 log.debug("Merge response: %s", merge_response)
1660 return merge_response, merge_response.possible, merge_response.merge_status_message
1664 return merge_response, merge_response.possible, merge_response.merge_status_message
1661 except NotImplementedError:
1665 except NotImplementedError:
1662 return None, False, _('Pull request merging is not supported.')
1666 return None, False, _('Pull request merging is not supported.')
1663
1667
1664 def _check_repo_requirements(self, target, source, translator):
1668 def _check_repo_requirements(self, target, source, translator):
1665 """
1669 """
1666 Check if `target` and `source` have compatible requirements.
1670 Check if `target` and `source` have compatible requirements.
1667
1671
1668 Currently this is just checking for largefiles.
1672 Currently this is just checking for largefiles.
1669 """
1673 """
1670 _ = translator
1674 _ = translator
1671 target_has_largefiles = self._has_largefiles(target)
1675 target_has_largefiles = self._has_largefiles(target)
1672 source_has_largefiles = self._has_largefiles(source)
1676 source_has_largefiles = self._has_largefiles(source)
1673 merge_possible = True
1677 merge_possible = True
1674 message = u''
1678 message = u''
1675
1679
1676 if target_has_largefiles != source_has_largefiles:
1680 if target_has_largefiles != source_has_largefiles:
1677 merge_possible = False
1681 merge_possible = False
1678 if source_has_largefiles:
1682 if source_has_largefiles:
1679 message = _(
1683 message = _(
1680 'Target repository large files support is disabled.')
1684 'Target repository large files support is disabled.')
1681 else:
1685 else:
1682 message = _(
1686 message = _(
1683 'Source repository large files support is disabled.')
1687 'Source repository large files support is disabled.')
1684
1688
1685 return merge_possible, message
1689 return merge_possible, message
1686
1690
1687 def _has_largefiles(self, repo):
1691 def _has_largefiles(self, repo):
1688 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1692 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1689 'extensions', 'largefiles')
1693 'extensions', 'largefiles')
1690 return largefiles_ui and largefiles_ui[0].active
1694 return largefiles_ui and largefiles_ui[0].active
1691
1695
1692 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1696 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1693 """
1697 """
1694 Try to merge the pull request and return the merge status.
1698 Try to merge the pull request and return the merge status.
1695 """
1699 """
1696 log.debug(
1700 log.debug(
1697 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1701 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1698 pull_request.pull_request_id, force_shadow_repo_refresh)
1702 pull_request.pull_request_id, force_shadow_repo_refresh)
1699 target_vcs = pull_request.target_repo.scm_instance()
1703 target_vcs = pull_request.target_repo.scm_instance()
1700 # Refresh the target reference.
1704 # Refresh the target reference.
1701 try:
1705 try:
1702 target_ref = self._refresh_reference(
1706 target_ref = self._refresh_reference(
1703 pull_request.target_ref_parts, target_vcs)
1707 pull_request.target_ref_parts, target_vcs)
1704 except CommitDoesNotExistError:
1708 except CommitDoesNotExistError:
1705 merge_state = MergeResponse(
1709 merge_state = MergeResponse(
1706 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1710 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1707 metadata={'target_ref': pull_request.target_ref_parts})
1711 metadata={'target_ref': pull_request.target_ref_parts})
1708 return merge_state
1712 return merge_state
1709
1713
1710 target_locked = pull_request.target_repo.locked
1714 target_locked = pull_request.target_repo.locked
1711 if target_locked and target_locked[0]:
1715 if target_locked and target_locked[0]:
1712 locked_by = 'user:{}'.format(target_locked[0])
1716 locked_by = 'user:{}'.format(target_locked[0])
1713 log.debug("The target repository is locked by %s.", locked_by)
1717 log.debug("The target repository is locked by %s.", locked_by)
1714 merge_state = MergeResponse(
1718 merge_state = MergeResponse(
1715 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1719 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1716 metadata={'locked_by': locked_by})
1720 metadata={'locked_by': locked_by})
1717 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1721 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1718 pull_request, target_ref):
1722 pull_request, target_ref):
1719 log.debug("Refreshing the merge status of the repository.")
1723 log.debug("Refreshing the merge status of the repository.")
1720 merge_state = self._refresh_merge_state(
1724 merge_state = self._refresh_merge_state(
1721 pull_request, target_vcs, target_ref)
1725 pull_request, target_vcs, target_ref)
1722 else:
1726 else:
1723 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1727 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1724 metadata = {
1728 metadata = {
1725 'unresolved_files': '',
1729 'unresolved_files': '',
1726 'target_ref': pull_request.target_ref_parts,
1730 'target_ref': pull_request.target_ref_parts,
1727 'source_ref': pull_request.source_ref_parts,
1731 'source_ref': pull_request.source_ref_parts,
1728 }
1732 }
1729 if pull_request.last_merge_metadata:
1733 if pull_request.last_merge_metadata:
1730 metadata.update(pull_request.last_merge_metadata_parsed)
1734 metadata.update(pull_request.last_merge_metadata_parsed)
1731
1735
1732 if not possible and target_ref.type == 'branch':
1736 if not possible and target_ref.type == 'branch':
1733 # NOTE(marcink): case for mercurial multiple heads on branch
1737 # NOTE(marcink): case for mercurial multiple heads on branch
1734 heads = target_vcs._heads(target_ref.name)
1738 heads = target_vcs._heads(target_ref.name)
1735 if len(heads) != 1:
1739 if len(heads) != 1:
1736 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1740 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1737 metadata.update({
1741 metadata.update({
1738 'heads': heads
1742 'heads': heads
1739 })
1743 })
1740
1744
1741 merge_state = MergeResponse(
1745 merge_state = MergeResponse(
1742 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1746 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1743
1747
1744 return merge_state
1748 return merge_state
1745
1749
1746 def _refresh_reference(self, reference, vcs_repository):
1750 def _refresh_reference(self, reference, vcs_repository):
1747 if reference.type in self.UPDATABLE_REF_TYPES:
1751 if reference.type in self.UPDATABLE_REF_TYPES:
1748 name_or_id = reference.name
1752 name_or_id = reference.name
1749 else:
1753 else:
1750 name_or_id = reference.commit_id
1754 name_or_id = reference.commit_id
1751
1755
1752 refreshed_commit = vcs_repository.get_commit(name_or_id)
1756 refreshed_commit = vcs_repository.get_commit(name_or_id)
1753 refreshed_reference = Reference(
1757 refreshed_reference = Reference(
1754 reference.type, reference.name, refreshed_commit.raw_id)
1758 reference.type, reference.name, refreshed_commit.raw_id)
1755 return refreshed_reference
1759 return refreshed_reference
1756
1760
1757 def _needs_merge_state_refresh(self, pull_request, target_reference):
1761 def _needs_merge_state_refresh(self, pull_request, target_reference):
1758 return not(
1762 return not(
1759 pull_request.revisions and
1763 pull_request.revisions and
1760 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1764 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1761 target_reference.commit_id == pull_request._last_merge_target_rev)
1765 target_reference.commit_id == pull_request._last_merge_target_rev)
1762
1766
1763 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1767 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1764 workspace_id = self._workspace_id(pull_request)
1768 workspace_id = self._workspace_id(pull_request)
1765 source_vcs = pull_request.source_repo.scm_instance()
1769 source_vcs = pull_request.source_repo.scm_instance()
1766 repo_id = pull_request.target_repo.repo_id
1770 repo_id = pull_request.target_repo.repo_id
1767 use_rebase = self._use_rebase_for_merging(pull_request)
1771 use_rebase = self._use_rebase_for_merging(pull_request)
1768 close_branch = self._close_branch_before_merging(pull_request)
1772 close_branch = self._close_branch_before_merging(pull_request)
1769 merge_state = target_vcs.merge(
1773 merge_state = target_vcs.merge(
1770 repo_id, workspace_id,
1774 repo_id, workspace_id,
1771 target_reference, source_vcs, pull_request.source_ref_parts,
1775 target_reference, source_vcs, pull_request.source_ref_parts,
1772 dry_run=True, use_rebase=use_rebase,
1776 dry_run=True, use_rebase=use_rebase,
1773 close_branch=close_branch)
1777 close_branch=close_branch)
1774
1778
1775 # Do not store the response if there was an unknown error.
1779 # Do not store the response if there was an unknown error.
1776 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1780 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1777 pull_request._last_merge_source_rev = \
1781 pull_request._last_merge_source_rev = \
1778 pull_request.source_ref_parts.commit_id
1782 pull_request.source_ref_parts.commit_id
1779 pull_request._last_merge_target_rev = target_reference.commit_id
1783 pull_request._last_merge_target_rev = target_reference.commit_id
1780 pull_request.last_merge_status = merge_state.failure_reason
1784 pull_request.last_merge_status = merge_state.failure_reason
1781 pull_request.last_merge_metadata = merge_state.metadata
1785 pull_request.last_merge_metadata = merge_state.metadata
1782
1786
1783 pull_request.shadow_merge_ref = merge_state.merge_ref
1787 pull_request.shadow_merge_ref = merge_state.merge_ref
1784 Session().add(pull_request)
1788 Session().add(pull_request)
1785 Session().commit()
1789 Session().commit()
1786
1790
1787 return merge_state
1791 return merge_state
1788
1792
1789 def _workspace_id(self, pull_request):
1793 def _workspace_id(self, pull_request):
1790 workspace_id = 'pr-%s' % pull_request.pull_request_id
1794 workspace_id = 'pr-%s' % pull_request.pull_request_id
1791 return workspace_id
1795 return workspace_id
1792
1796
1793 def generate_repo_data(self, repo, commit_id=None, branch=None,
1797 def generate_repo_data(self, repo, commit_id=None, branch=None,
1794 bookmark=None, translator=None):
1798 bookmark=None, translator=None):
1795 from rhodecode.model.repo import RepoModel
1799 from rhodecode.model.repo import RepoModel
1796
1800
1797 all_refs, selected_ref = \
1801 all_refs, selected_ref = \
1798 self._get_repo_pullrequest_sources(
1802 self._get_repo_pullrequest_sources(
1799 repo.scm_instance(), commit_id=commit_id,
1803 repo.scm_instance(), commit_id=commit_id,
1800 branch=branch, bookmark=bookmark, translator=translator)
1804 branch=branch, bookmark=bookmark, translator=translator)
1801
1805
1802 refs_select2 = []
1806 refs_select2 = []
1803 for element in all_refs:
1807 for element in all_refs:
1804 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1808 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1805 refs_select2.append({'text': element[1], 'children': children})
1809 refs_select2.append({'text': element[1], 'children': children})
1806
1810
1807 return {
1811 return {
1808 'user': {
1812 'user': {
1809 'user_id': repo.user.user_id,
1813 'user_id': repo.user.user_id,
1810 'username': repo.user.username,
1814 'username': repo.user.username,
1811 'firstname': repo.user.first_name,
1815 'firstname': repo.user.first_name,
1812 'lastname': repo.user.last_name,
1816 'lastname': repo.user.last_name,
1813 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1817 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1814 },
1818 },
1815 'name': repo.repo_name,
1819 'name': repo.repo_name,
1816 'link': RepoModel().get_url(repo),
1820 'link': RepoModel().get_url(repo),
1817 'description': h.chop_at_smart(repo.description_safe, '\n'),
1821 'description': h.chop_at_smart(repo.description_safe, '\n'),
1818 'refs': {
1822 'refs': {
1819 'all_refs': all_refs,
1823 'all_refs': all_refs,
1820 'selected_ref': selected_ref,
1824 'selected_ref': selected_ref,
1821 'select2_refs': refs_select2
1825 'select2_refs': refs_select2
1822 }
1826 }
1823 }
1827 }
1824
1828
1825 def generate_pullrequest_title(self, source, source_ref, target):
1829 def generate_pullrequest_title(self, source, source_ref, target):
1826 return u'{source}#{at_ref} to {target}'.format(
1830 return u'{source}#{at_ref} to {target}'.format(
1827 source=source,
1831 source=source,
1828 at_ref=source_ref,
1832 at_ref=source_ref,
1829 target=target,
1833 target=target,
1830 )
1834 )
1831
1835
1832 def _cleanup_merge_workspace(self, pull_request):
1836 def _cleanup_merge_workspace(self, pull_request):
1833 # Merging related cleanup
1837 # Merging related cleanup
1834 repo_id = pull_request.target_repo.repo_id
1838 repo_id = pull_request.target_repo.repo_id
1835 target_scm = pull_request.target_repo.scm_instance()
1839 target_scm = pull_request.target_repo.scm_instance()
1836 workspace_id = self._workspace_id(pull_request)
1840 workspace_id = self._workspace_id(pull_request)
1837
1841
1838 try:
1842 try:
1839 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1843 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1840 except NotImplementedError:
1844 except NotImplementedError:
1841 pass
1845 pass
1842
1846
1843 def _get_repo_pullrequest_sources(
1847 def _get_repo_pullrequest_sources(
1844 self, repo, commit_id=None, branch=None, bookmark=None,
1848 self, repo, commit_id=None, branch=None, bookmark=None,
1845 translator=None):
1849 translator=None):
1846 """
1850 """
1847 Return a structure with repo's interesting commits, suitable for
1851 Return a structure with repo's interesting commits, suitable for
1848 the selectors in pullrequest controller
1852 the selectors in pullrequest controller
1849
1853
1850 :param commit_id: a commit that must be in the list somehow
1854 :param commit_id: a commit that must be in the list somehow
1851 and selected by default
1855 and selected by default
1852 :param branch: a branch that must be in the list and selected
1856 :param branch: a branch that must be in the list and selected
1853 by default - even if closed
1857 by default - even if closed
1854 :param bookmark: a bookmark that must be in the list and selected
1858 :param bookmark: a bookmark that must be in the list and selected
1855 """
1859 """
1856 _ = translator or get_current_request().translate
1860 _ = translator or get_current_request().translate
1857
1861
1858 commit_id = safe_str(commit_id) if commit_id else None
1862 commit_id = safe_str(commit_id) if commit_id else None
1859 branch = safe_unicode(branch) if branch else None
1863 branch = safe_unicode(branch) if branch else None
1860 bookmark = safe_unicode(bookmark) if bookmark else None
1864 bookmark = safe_unicode(bookmark) if bookmark else None
1861
1865
1862 selected = None
1866 selected = None
1863
1867
1864 # order matters: first source that has commit_id in it will be selected
1868 # order matters: first source that has commit_id in it will be selected
1865 sources = []
1869 sources = []
1866 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1870 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1867 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1871 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1868
1872
1869 if commit_id:
1873 if commit_id:
1870 ref_commit = (h.short_id(commit_id), commit_id)
1874 ref_commit = (h.short_id(commit_id), commit_id)
1871 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1875 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1872
1876
1873 sources.append(
1877 sources.append(
1874 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1878 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1875 )
1879 )
1876
1880
1877 groups = []
1881 groups = []
1878
1882
1879 for group_key, ref_list, group_name, match in sources:
1883 for group_key, ref_list, group_name, match in sources:
1880 group_refs = []
1884 group_refs = []
1881 for ref_name, ref_id in ref_list:
1885 for ref_name, ref_id in ref_list:
1882 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1886 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1883 group_refs.append((ref_key, ref_name))
1887 group_refs.append((ref_key, ref_name))
1884
1888
1885 if not selected:
1889 if not selected:
1886 if set([commit_id, match]) & set([ref_id, ref_name]):
1890 if set([commit_id, match]) & set([ref_id, ref_name]):
1887 selected = ref_key
1891 selected = ref_key
1888
1892
1889 if group_refs:
1893 if group_refs:
1890 groups.append((group_refs, group_name))
1894 groups.append((group_refs, group_name))
1891
1895
1892 if not selected:
1896 if not selected:
1893 ref = commit_id or branch or bookmark
1897 ref = commit_id or branch or bookmark
1894 if ref:
1898 if ref:
1895 raise CommitDoesNotExistError(
1899 raise CommitDoesNotExistError(
1896 u'No commit refs could be found matching: {}'.format(ref))
1900 u'No commit refs could be found matching: {}'.format(ref))
1897 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1901 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1898 selected = u'branch:{}:{}'.format(
1902 selected = u'branch:{}:{}'.format(
1899 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1903 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1900 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1904 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1901 )
1905 )
1902 elif repo.commit_ids:
1906 elif repo.commit_ids:
1903 # make the user select in this case
1907 # make the user select in this case
1904 selected = None
1908 selected = None
1905 else:
1909 else:
1906 raise EmptyRepositoryError()
1910 raise EmptyRepositoryError()
1907 return groups, selected
1911 return groups, selected
1908
1912
1909 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1913 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1910 hide_whitespace_changes, diff_context):
1914 hide_whitespace_changes, diff_context):
1911
1915
1912 return self._get_diff_from_pr_or_version(
1916 return self._get_diff_from_pr_or_version(
1913 source_repo, source_ref_id, target_ref_id,
1917 source_repo, source_ref_id, target_ref_id,
1914 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1918 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1915
1919
1916 def _get_diff_from_pr_or_version(
1920 def _get_diff_from_pr_or_version(
1917 self, source_repo, source_ref_id, target_ref_id,
1921 self, source_repo, source_ref_id, target_ref_id,
1918 hide_whitespace_changes, diff_context):
1922 hide_whitespace_changes, diff_context):
1919
1923
1920 target_commit = source_repo.get_commit(
1924 target_commit = source_repo.get_commit(
1921 commit_id=safe_str(target_ref_id))
1925 commit_id=safe_str(target_ref_id))
1922 source_commit = source_repo.get_commit(
1926 source_commit = source_repo.get_commit(
1923 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1927 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1924 if isinstance(source_repo, Repository):
1928 if isinstance(source_repo, Repository):
1925 vcs_repo = source_repo.scm_instance()
1929 vcs_repo = source_repo.scm_instance()
1926 else:
1930 else:
1927 vcs_repo = source_repo
1931 vcs_repo = source_repo
1928
1932
1929 # TODO: johbo: In the context of an update, we cannot reach
1933 # TODO: johbo: In the context of an update, we cannot reach
1930 # the old commit anymore with our normal mechanisms. It needs
1934 # the old commit anymore with our normal mechanisms. It needs
1931 # some sort of special support in the vcs layer to avoid this
1935 # some sort of special support in the vcs layer to avoid this
1932 # workaround.
1936 # workaround.
1933 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1937 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1934 vcs_repo.alias == 'git'):
1938 vcs_repo.alias == 'git'):
1935 source_commit.raw_id = safe_str(source_ref_id)
1939 source_commit.raw_id = safe_str(source_ref_id)
1936
1940
1937 log.debug('calculating diff between '
1941 log.debug('calculating diff between '
1938 'source_ref:%s and target_ref:%s for repo `%s`',
1942 'source_ref:%s and target_ref:%s for repo `%s`',
1939 target_ref_id, source_ref_id,
1943 target_ref_id, source_ref_id,
1940 safe_unicode(vcs_repo.path))
1944 safe_unicode(vcs_repo.path))
1941
1945
1942 vcs_diff = vcs_repo.get_diff(
1946 vcs_diff = vcs_repo.get_diff(
1943 commit1=target_commit, commit2=source_commit,
1947 commit1=target_commit, commit2=source_commit,
1944 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1948 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1945 return vcs_diff
1949 return vcs_diff
1946
1950
1947 def _is_merge_enabled(self, pull_request):
1951 def _is_merge_enabled(self, pull_request):
1948 return self._get_general_setting(
1952 return self._get_general_setting(
1949 pull_request, 'rhodecode_pr_merge_enabled')
1953 pull_request, 'rhodecode_pr_merge_enabled')
1950
1954
1951 def _use_rebase_for_merging(self, pull_request):
1955 def _use_rebase_for_merging(self, pull_request):
1952 repo_type = pull_request.target_repo.repo_type
1956 repo_type = pull_request.target_repo.repo_type
1953 if repo_type == 'hg':
1957 if repo_type == 'hg':
1954 return self._get_general_setting(
1958 return self._get_general_setting(
1955 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1959 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1956 elif repo_type == 'git':
1960 elif repo_type == 'git':
1957 return self._get_general_setting(
1961 return self._get_general_setting(
1958 pull_request, 'rhodecode_git_use_rebase_for_merging')
1962 pull_request, 'rhodecode_git_use_rebase_for_merging')
1959
1963
1960 return False
1964 return False
1961
1965
1962 def _user_name_for_merging(self, pull_request, user):
1966 def _user_name_for_merging(self, pull_request, user):
1963 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1967 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1964 if env_user_name_attr and hasattr(user, env_user_name_attr):
1968 if env_user_name_attr and hasattr(user, env_user_name_attr):
1965 user_name_attr = env_user_name_attr
1969 user_name_attr = env_user_name_attr
1966 else:
1970 else:
1967 user_name_attr = 'short_contact'
1971 user_name_attr = 'short_contact'
1968
1972
1969 user_name = getattr(user, user_name_attr)
1973 user_name = getattr(user, user_name_attr)
1970 return user_name
1974 return user_name
1971
1975
1972 def _close_branch_before_merging(self, pull_request):
1976 def _close_branch_before_merging(self, pull_request):
1973 repo_type = pull_request.target_repo.repo_type
1977 repo_type = pull_request.target_repo.repo_type
1974 if repo_type == 'hg':
1978 if repo_type == 'hg':
1975 return self._get_general_setting(
1979 return self._get_general_setting(
1976 pull_request, 'rhodecode_hg_close_branch_before_merging')
1980 pull_request, 'rhodecode_hg_close_branch_before_merging')
1977 elif repo_type == 'git':
1981 elif repo_type == 'git':
1978 return self._get_general_setting(
1982 return self._get_general_setting(
1979 pull_request, 'rhodecode_git_close_branch_before_merging')
1983 pull_request, 'rhodecode_git_close_branch_before_merging')
1980
1984
1981 return False
1985 return False
1982
1986
1983 def _get_general_setting(self, pull_request, settings_key, default=False):
1987 def _get_general_setting(self, pull_request, settings_key, default=False):
1984 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1988 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1985 settings = settings_model.get_general_settings()
1989 settings = settings_model.get_general_settings()
1986 return settings.get(settings_key, default)
1990 return settings.get(settings_key, default)
1987
1991
1988 def _log_audit_action(self, action, action_data, user, pull_request):
1992 def _log_audit_action(self, action, action_data, user, pull_request):
1989 audit_logger.store(
1993 audit_logger.store(
1990 action=action,
1994 action=action,
1991 action_data=action_data,
1995 action_data=action_data,
1992 user=user,
1996 user=user,
1993 repo=pull_request.target_repo)
1997 repo=pull_request.target_repo)
1994
1998
1995 def get_reviewer_functions(self):
1999 def get_reviewer_functions(self):
1996 """
2000 """
1997 Fetches functions for validation and fetching default reviewers.
2001 Fetches functions for validation and fetching default reviewers.
1998 If available we use the EE package, else we fallback to CE
2002 If available we use the EE package, else we fallback to CE
1999 package functions
2003 package functions
2000 """
2004 """
2001 try:
2005 try:
2002 from rc_reviewers.utils import get_default_reviewers_data
2006 from rc_reviewers.utils import get_default_reviewers_data
2003 from rc_reviewers.utils import validate_default_reviewers
2007 from rc_reviewers.utils import validate_default_reviewers
2004 from rc_reviewers.utils import validate_observers
2008 from rc_reviewers.utils import validate_observers
2005 except ImportError:
2009 except ImportError:
2006 from rhodecode.apps.repository.utils import get_default_reviewers_data
2010 from rhodecode.apps.repository.utils import get_default_reviewers_data
2007 from rhodecode.apps.repository.utils import validate_default_reviewers
2011 from rhodecode.apps.repository.utils import validate_default_reviewers
2008 from rhodecode.apps.repository.utils import validate_observers
2012 from rhodecode.apps.repository.utils import validate_observers
2009
2013
2010 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2014 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2011
2015
2012
2016
2013 class MergeCheck(object):
2017 class MergeCheck(object):
2014 """
2018 """
2015 Perform Merge Checks and returns a check object which stores information
2019 Perform Merge Checks and returns a check object which stores information
2016 about merge errors, and merge conditions
2020 about merge errors, and merge conditions
2017 """
2021 """
2018 TODO_CHECK = 'todo'
2022 TODO_CHECK = 'todo'
2019 PERM_CHECK = 'perm'
2023 PERM_CHECK = 'perm'
2020 REVIEW_CHECK = 'review'
2024 REVIEW_CHECK = 'review'
2021 MERGE_CHECK = 'merge'
2025 MERGE_CHECK = 'merge'
2022 WIP_CHECK = 'wip'
2026 WIP_CHECK = 'wip'
2023
2027
2024 def __init__(self):
2028 def __init__(self):
2025 self.review_status = None
2029 self.review_status = None
2026 self.merge_possible = None
2030 self.merge_possible = None
2027 self.merge_msg = ''
2031 self.merge_msg = ''
2028 self.merge_response = None
2032 self.merge_response = None
2029 self.failed = None
2033 self.failed = None
2030 self.errors = []
2034 self.errors = []
2031 self.error_details = OrderedDict()
2035 self.error_details = OrderedDict()
2032 self.source_commit = AttributeDict()
2036 self.source_commit = AttributeDict()
2033 self.target_commit = AttributeDict()
2037 self.target_commit = AttributeDict()
2034
2038
2035 def __repr__(self):
2039 def __repr__(self):
2036 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2040 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2037 self.merge_possible, self.failed, self.errors)
2041 self.merge_possible, self.failed, self.errors)
2038
2042
2039 def push_error(self, error_type, message, error_key, details):
2043 def push_error(self, error_type, message, error_key, details):
2040 self.failed = True
2044 self.failed = True
2041 self.errors.append([error_type, message])
2045 self.errors.append([error_type, message])
2042 self.error_details[error_key] = dict(
2046 self.error_details[error_key] = dict(
2043 details=details,
2047 details=details,
2044 error_type=error_type,
2048 error_type=error_type,
2045 message=message
2049 message=message
2046 )
2050 )
2047
2051
2048 @classmethod
2052 @classmethod
2049 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2053 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2050 force_shadow_repo_refresh=False):
2054 force_shadow_repo_refresh=False):
2051 _ = translator
2055 _ = translator
2052 merge_check = cls()
2056 merge_check = cls()
2053
2057
2054 # title has WIP:
2058 # title has WIP:
2055 if pull_request.work_in_progress:
2059 if pull_request.work_in_progress:
2056 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2060 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2057
2061
2058 msg = _('WIP marker in title prevents from accidental merge.')
2062 msg = _('WIP marker in title prevents from accidental merge.')
2059 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2063 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2060 if fail_early:
2064 if fail_early:
2061 return merge_check
2065 return merge_check
2062
2066
2063 # permissions to merge
2067 # permissions to merge
2064 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2068 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2065 if not user_allowed_to_merge:
2069 if not user_allowed_to_merge:
2066 log.debug("MergeCheck: cannot merge, approval is pending.")
2070 log.debug("MergeCheck: cannot merge, approval is pending.")
2067
2071
2068 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2072 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2069 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2073 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2070 if fail_early:
2074 if fail_early:
2071 return merge_check
2075 return merge_check
2072
2076
2073 # permission to merge into the target branch
2077 # permission to merge into the target branch
2074 target_commit_id = pull_request.target_ref_parts.commit_id
2078 target_commit_id = pull_request.target_ref_parts.commit_id
2075 if pull_request.target_ref_parts.type == 'branch':
2079 if pull_request.target_ref_parts.type == 'branch':
2076 branch_name = pull_request.target_ref_parts.name
2080 branch_name = pull_request.target_ref_parts.name
2077 else:
2081 else:
2078 # for mercurial we can always figure out the branch from the commit
2082 # for mercurial we can always figure out the branch from the commit
2079 # in case of bookmark
2083 # in case of bookmark
2080 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2084 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2081 branch_name = target_commit.branch
2085 branch_name = target_commit.branch
2082
2086
2083 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2087 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2084 pull_request.target_repo.repo_name, branch_name)
2088 pull_request.target_repo.repo_name, branch_name)
2085 if branch_perm and branch_perm == 'branch.none':
2089 if branch_perm and branch_perm == 'branch.none':
2086 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2090 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2087 branch_name, rule)
2091 branch_name, rule)
2088 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2092 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2089 if fail_early:
2093 if fail_early:
2090 return merge_check
2094 return merge_check
2091
2095
2092 # review status, must be always present
2096 # review status, must be always present
2093 review_status = pull_request.calculated_review_status()
2097 review_status = pull_request.calculated_review_status()
2094 merge_check.review_status = review_status
2098 merge_check.review_status = review_status
2095
2099
2096 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2100 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2097 if not status_approved:
2101 if not status_approved:
2098 log.debug("MergeCheck: cannot merge, approval is pending.")
2102 log.debug("MergeCheck: cannot merge, approval is pending.")
2099
2103
2100 msg = _('Pull request reviewer approval is pending.')
2104 msg = _('Pull request reviewer approval is pending.')
2101
2105
2102 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2106 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2103
2107
2104 if fail_early:
2108 if fail_early:
2105 return merge_check
2109 return merge_check
2106
2110
2107 # left over TODOs
2111 # left over TODOs
2108 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2112 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2109 if todos:
2113 if todos:
2110 log.debug("MergeCheck: cannot merge, {} "
2114 log.debug("MergeCheck: cannot merge, {} "
2111 "unresolved TODOs left.".format(len(todos)))
2115 "unresolved TODOs left.".format(len(todos)))
2112
2116
2113 if len(todos) == 1:
2117 if len(todos) == 1:
2114 msg = _('Cannot merge, {} TODO still not resolved.').format(
2118 msg = _('Cannot merge, {} TODO still not resolved.').format(
2115 len(todos))
2119 len(todos))
2116 else:
2120 else:
2117 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2121 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2118 len(todos))
2122 len(todos))
2119
2123
2120 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2124 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2121
2125
2122 if fail_early:
2126 if fail_early:
2123 return merge_check
2127 return merge_check
2124
2128
2125 # merge possible, here is the filesystem simulation + shadow repo
2129 # merge possible, here is the filesystem simulation + shadow repo
2126 merge_response, merge_status, msg = PullRequestModel().merge_status(
2130 merge_response, merge_status, msg = PullRequestModel().merge_status(
2127 pull_request, translator=translator,
2131 pull_request, translator=translator,
2128 force_shadow_repo_refresh=force_shadow_repo_refresh)
2132 force_shadow_repo_refresh=force_shadow_repo_refresh)
2129
2133
2130 merge_check.merge_possible = merge_status
2134 merge_check.merge_possible = merge_status
2131 merge_check.merge_msg = msg
2135 merge_check.merge_msg = msg
2132 merge_check.merge_response = merge_response
2136 merge_check.merge_response = merge_response
2133
2137
2134 source_ref_id = pull_request.source_ref_parts.commit_id
2138 source_ref_id = pull_request.source_ref_parts.commit_id
2135 target_ref_id = pull_request.target_ref_parts.commit_id
2139 target_ref_id = pull_request.target_ref_parts.commit_id
2136
2140
2137 try:
2141 try:
2138 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2142 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2139 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2143 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2140 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2144 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2141 merge_check.source_commit.current_raw_id = source_commit.raw_id
2145 merge_check.source_commit.current_raw_id = source_commit.raw_id
2142 merge_check.source_commit.previous_raw_id = source_ref_id
2146 merge_check.source_commit.previous_raw_id = source_ref_id
2143
2147
2144 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2148 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2145 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2149 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2146 merge_check.target_commit.current_raw_id = target_commit.raw_id
2150 merge_check.target_commit.current_raw_id = target_commit.raw_id
2147 merge_check.target_commit.previous_raw_id = target_ref_id
2151 merge_check.target_commit.previous_raw_id = target_ref_id
2148 except (SourceRefMissing, TargetRefMissing):
2152 except (SourceRefMissing, TargetRefMissing):
2149 pass
2153 pass
2150
2154
2151 if not merge_status:
2155 if not merge_status:
2152 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2156 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2153 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2157 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2154
2158
2155 if fail_early:
2159 if fail_early:
2156 return merge_check
2160 return merge_check
2157
2161
2158 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2162 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2159 return merge_check
2163 return merge_check
2160
2164
2161 @classmethod
2165 @classmethod
2162 def get_merge_conditions(cls, pull_request, translator):
2166 def get_merge_conditions(cls, pull_request, translator):
2163 _ = translator
2167 _ = translator
2164 merge_details = {}
2168 merge_details = {}
2165
2169
2166 model = PullRequestModel()
2170 model = PullRequestModel()
2167 use_rebase = model._use_rebase_for_merging(pull_request)
2171 use_rebase = model._use_rebase_for_merging(pull_request)
2168
2172
2169 if use_rebase:
2173 if use_rebase:
2170 merge_details['merge_strategy'] = dict(
2174 merge_details['merge_strategy'] = dict(
2171 details={},
2175 details={},
2172 message=_('Merge strategy: rebase')
2176 message=_('Merge strategy: rebase')
2173 )
2177 )
2174 else:
2178 else:
2175 merge_details['merge_strategy'] = dict(
2179 merge_details['merge_strategy'] = dict(
2176 details={},
2180 details={},
2177 message=_('Merge strategy: explicit merge commit')
2181 message=_('Merge strategy: explicit merge commit')
2178 )
2182 )
2179
2183
2180 close_branch = model._close_branch_before_merging(pull_request)
2184 close_branch = model._close_branch_before_merging(pull_request)
2181 if close_branch:
2185 if close_branch:
2182 repo_type = pull_request.target_repo.repo_type
2186 repo_type = pull_request.target_repo.repo_type
2183 close_msg = ''
2187 close_msg = ''
2184 if repo_type == 'hg':
2188 if repo_type == 'hg':
2185 close_msg = _('Source branch will be closed before the merge.')
2189 close_msg = _('Source branch will be closed before the merge.')
2186 elif repo_type == 'git':
2190 elif repo_type == 'git':
2187 close_msg = _('Source branch will be deleted after the merge.')
2191 close_msg = _('Source branch will be deleted after the merge.')
2188
2192
2189 merge_details['close_branch'] = dict(
2193 merge_details['close_branch'] = dict(
2190 details={},
2194 details={},
2191 message=close_msg
2195 message=close_msg
2192 )
2196 )
2193
2197
2194 return merge_details
2198 return merge_details
2195
2199
2196
2200
2197 ChangeTuple = collections.namedtuple(
2201 ChangeTuple = collections.namedtuple(
2198 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2202 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2199
2203
2200 FileChangeTuple = collections.namedtuple(
2204 FileChangeTuple = collections.namedtuple(
2201 'FileChangeTuple', ['added', 'modified', 'removed'])
2205 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now