##// END OF EJS Templates
pull-requests: fixed creation of pr after new serialized commits data.
marcink -
r4517:7cc4ee55 stable
parent child
Show More
@@ -1,2235 +1,2237
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 log.debug('Obtaining commit authors from set of commits')
158 _compare_data = target_scm.compare(
158 _compare_data = target_scm.compare(
159 target_ref, source_ref, source_scm, merge=True,
159 target_ref, source_ref, source_scm, merge=True,
160 pre_load=["author", "date", "message"]
160 pre_load=["author", "date", "message"]
161 )
161 )
162
162
163 for commit in _compare_data:
163 for commit in _compare_data:
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
165 # at this function which is later called via JSON serialization
165 # at this function which is later called via JSON serialization
166 serialized_commit = dict(
166 serialized_commit = dict(
167 author=commit.author,
167 author=commit.author,
168 date=commit.date,
168 date=commit.date,
169 message=commit.message,
169 message=commit.message,
170 commit_id=commit.raw_id,
171 raw_id=commit.raw_id
170 )
172 )
171 commits.append(serialized_commit)
173 commits.append(serialized_commit)
172 user = User.get_from_cs_author(serialized_commit['author'])
174 user = User.get_from_cs_author(serialized_commit['author'])
173 if user and user not in commit_authors:
175 if user and user not in commit_authors:
174 commit_authors.append(user)
176 commit_authors.append(user)
175
177
176 # lines
178 # lines
177 if get_authors:
179 if get_authors:
178 log.debug('Calculating authors of changed files')
180 log.debug('Calculating authors of changed files')
179 target_commit = source_repo.get_commit(ancestor_id)
181 target_commit = source_repo.get_commit(ancestor_id)
180
182
181 for fname, lines in changed_lines.items():
183 for fname, lines in changed_lines.items():
182
184
183 try:
185 try:
184 node = target_commit.get_node(fname, pre_load=["is_binary"])
186 node = target_commit.get_node(fname, pre_load=["is_binary"])
185 except Exception:
187 except Exception:
186 log.exception("Failed to load node with path %s", fname)
188 log.exception("Failed to load node with path %s", fname)
187 continue
189 continue
188
190
189 if not isinstance(node, FileNode):
191 if not isinstance(node, FileNode):
190 continue
192 continue
191
193
192 # NOTE(marcink): for binary node we don't do annotation, just use last author
194 # NOTE(marcink): for binary node we don't do annotation, just use last author
193 if node.is_binary:
195 if node.is_binary:
194 author = node.last_commit.author
196 author = node.last_commit.author
195 email = node.last_commit.author_email
197 email = node.last_commit.author_email
196
198
197 user = User.get_from_cs_author(author)
199 user = User.get_from_cs_author(author)
198 if user:
200 if user:
199 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
200 author_counts[author] = author_counts.get(author, 0) + 1
202 author_counts[author] = author_counts.get(author, 0) + 1
201 email_counts[email] = email_counts.get(email, 0) + 1
203 email_counts[email] = email_counts.get(email, 0) + 1
202
204
203 continue
205 continue
204
206
205 for annotation in node.annotate:
207 for annotation in node.annotate:
206 line_no, commit_id, get_commit_func, line_text = annotation
208 line_no, commit_id, get_commit_func, line_text = annotation
207 if line_no in lines:
209 if line_no in lines:
208 if commit_id not in _commit_cache:
210 if commit_id not in _commit_cache:
209 _commit_cache[commit_id] = get_commit_func()
211 _commit_cache[commit_id] = get_commit_func()
210 commit = _commit_cache[commit_id]
212 commit = _commit_cache[commit_id]
211 author = commit.author
213 author = commit.author
212 email = commit.author_email
214 email = commit.author_email
213 user = User.get_from_cs_author(author)
215 user = User.get_from_cs_author(author)
214 if user:
216 if user:
215 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
217 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
216 author_counts[author] = author_counts.get(author, 0) + 1
218 author_counts[author] = author_counts.get(author, 0) + 1
217 email_counts[email] = email_counts.get(email, 0) + 1
219 email_counts[email] = email_counts.get(email, 0) + 1
218
220
219 log.debug('Default reviewers processing finished')
221 log.debug('Default reviewers processing finished')
220
222
221 return {
223 return {
222 'commits': commits,
224 'commits': commits,
223 'files': all_files_changes,
225 'files': all_files_changes,
224 'stats': stats,
226 'stats': stats,
225 'ancestor': ancestor_id,
227 'ancestor': ancestor_id,
226 # original authors of modified files
228 # original authors of modified files
227 'original_authors': {
229 'original_authors': {
228 'users': user_counts,
230 'users': user_counts,
229 'authors': author_counts,
231 'authors': author_counts,
230 'emails': email_counts,
232 'emails': email_counts,
231 },
233 },
232 'commit_authors': commit_authors
234 'commit_authors': commit_authors
233 }
235 }
234
236
235
237
236 class PullRequestModel(BaseModel):
238 class PullRequestModel(BaseModel):
237
239
238 cls = PullRequest
240 cls = PullRequest
239
241
240 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
242 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
241
243
242 UPDATE_STATUS_MESSAGES = {
244 UPDATE_STATUS_MESSAGES = {
243 UpdateFailureReason.NONE: lazy_ugettext(
245 UpdateFailureReason.NONE: lazy_ugettext(
244 'Pull request update successful.'),
246 'Pull request update successful.'),
245 UpdateFailureReason.UNKNOWN: lazy_ugettext(
247 UpdateFailureReason.UNKNOWN: lazy_ugettext(
246 'Pull request update failed because of an unknown error.'),
248 'Pull request update failed because of an unknown error.'),
247 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
249 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
248 'No update needed because the source and target have not changed.'),
250 'No update needed because the source and target have not changed.'),
249 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
251 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
250 'Pull request cannot be updated because the reference type is '
252 'Pull request cannot be updated because the reference type is '
251 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
253 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
252 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
254 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
253 'This pull request cannot be updated because the target '
255 'This pull request cannot be updated because the target '
254 'reference is missing.'),
256 'reference is missing.'),
255 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
257 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
256 'This pull request cannot be updated because the source '
258 'This pull request cannot be updated because the source '
257 'reference is missing.'),
259 'reference is missing.'),
258 }
260 }
259 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
261 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
260 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
262 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
261
263
262 def __get_pull_request(self, pull_request):
264 def __get_pull_request(self, pull_request):
263 return self._get_instance((
265 return self._get_instance((
264 PullRequest, PullRequestVersion), pull_request)
266 PullRequest, PullRequestVersion), pull_request)
265
267
266 def _check_perms(self, perms, pull_request, user, api=False):
268 def _check_perms(self, perms, pull_request, user, api=False):
267 if not api:
269 if not api:
268 return h.HasRepoPermissionAny(*perms)(
270 return h.HasRepoPermissionAny(*perms)(
269 user=user, repo_name=pull_request.target_repo.repo_name)
271 user=user, repo_name=pull_request.target_repo.repo_name)
270 else:
272 else:
271 return h.HasRepoPermissionAnyApi(*perms)(
273 return h.HasRepoPermissionAnyApi(*perms)(
272 user=user, repo_name=pull_request.target_repo.repo_name)
274 user=user, repo_name=pull_request.target_repo.repo_name)
273
275
274 def check_user_read(self, pull_request, user, api=False):
276 def check_user_read(self, pull_request, user, api=False):
275 _perms = ('repository.admin', 'repository.write', 'repository.read',)
277 _perms = ('repository.admin', 'repository.write', 'repository.read',)
276 return self._check_perms(_perms, pull_request, user, api)
278 return self._check_perms(_perms, pull_request, user, api)
277
279
278 def check_user_merge(self, pull_request, user, api=False):
280 def check_user_merge(self, pull_request, user, api=False):
279 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
281 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
280 return self._check_perms(_perms, pull_request, user, api)
282 return self._check_perms(_perms, pull_request, user, api)
281
283
282 def check_user_update(self, pull_request, user, api=False):
284 def check_user_update(self, pull_request, user, api=False):
283 owner = user.user_id == pull_request.user_id
285 owner = user.user_id == pull_request.user_id
284 return self.check_user_merge(pull_request, user, api) or owner
286 return self.check_user_merge(pull_request, user, api) or owner
285
287
286 def check_user_delete(self, pull_request, user):
288 def check_user_delete(self, pull_request, user):
287 owner = user.user_id == pull_request.user_id
289 owner = user.user_id == pull_request.user_id
288 _perms = ('repository.admin',)
290 _perms = ('repository.admin',)
289 return self._check_perms(_perms, pull_request, user) or owner
291 return self._check_perms(_perms, pull_request, user) or owner
290
292
291 def is_user_reviewer(self, pull_request, user):
293 def is_user_reviewer(self, pull_request, user):
292 return user.user_id in [
294 return user.user_id in [
293 x.user_id for x in
295 x.user_id for x in
294 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
295 if x.user
297 if x.user
296 ]
298 ]
297
299
298 def check_user_change_status(self, pull_request, user, api=False):
300 def check_user_change_status(self, pull_request, user, api=False):
299 return self.check_user_update(pull_request, user, api) \
301 return self.check_user_update(pull_request, user, api) \
300 or self.is_user_reviewer(pull_request, user)
302 or self.is_user_reviewer(pull_request, user)
301
303
302 def check_user_comment(self, pull_request, user):
304 def check_user_comment(self, pull_request, user):
303 owner = user.user_id == pull_request.user_id
305 owner = user.user_id == pull_request.user_id
304 return self.check_user_read(pull_request, user) or owner
306 return self.check_user_read(pull_request, user) or owner
305
307
306 def get(self, pull_request):
308 def get(self, pull_request):
307 return self.__get_pull_request(pull_request)
309 return self.__get_pull_request(pull_request)
308
310
309 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
311 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
310 statuses=None, opened_by=None, order_by=None,
312 statuses=None, opened_by=None, order_by=None,
311 order_dir='desc', only_created=False):
313 order_dir='desc', only_created=False):
312 repo = None
314 repo = None
313 if repo_name:
315 if repo_name:
314 repo = self._get_repo(repo_name)
316 repo = self._get_repo(repo_name)
315
317
316 q = PullRequest.query()
318 q = PullRequest.query()
317
319
318 if search_q:
320 if search_q:
319 like_expression = u'%{}%'.format(safe_unicode(search_q))
321 like_expression = u'%{}%'.format(safe_unicode(search_q))
320 q = q.join(User)
322 q = q.join(User)
321 q = q.filter(or_(
323 q = q.filter(or_(
322 cast(PullRequest.pull_request_id, String).ilike(like_expression),
324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
323 User.username.ilike(like_expression),
325 User.username.ilike(like_expression),
324 PullRequest.title.ilike(like_expression),
326 PullRequest.title.ilike(like_expression),
325 PullRequest.description.ilike(like_expression),
327 PullRequest.description.ilike(like_expression),
326 ))
328 ))
327
329
328 # source or target
330 # source or target
329 if repo and source:
331 if repo and source:
330 q = q.filter(PullRequest.source_repo == repo)
332 q = q.filter(PullRequest.source_repo == repo)
331 elif repo:
333 elif repo:
332 q = q.filter(PullRequest.target_repo == repo)
334 q = q.filter(PullRequest.target_repo == repo)
333
335
334 # closed,opened
336 # closed,opened
335 if statuses:
337 if statuses:
336 q = q.filter(PullRequest.status.in_(statuses))
338 q = q.filter(PullRequest.status.in_(statuses))
337
339
338 # opened by filter
340 # opened by filter
339 if opened_by:
341 if opened_by:
340 q = q.filter(PullRequest.user_id.in_(opened_by))
342 q = q.filter(PullRequest.user_id.in_(opened_by))
341
343
342 # only get those that are in "created" state
344 # only get those that are in "created" state
343 if only_created:
345 if only_created:
344 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
346 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
345
347
346 if order_by:
348 if order_by:
347 order_map = {
349 order_map = {
348 'name_raw': PullRequest.pull_request_id,
350 'name_raw': PullRequest.pull_request_id,
349 'id': PullRequest.pull_request_id,
351 'id': PullRequest.pull_request_id,
350 'title': PullRequest.title,
352 'title': PullRequest.title,
351 'updated_on_raw': PullRequest.updated_on,
353 'updated_on_raw': PullRequest.updated_on,
352 'target_repo': PullRequest.target_repo_id
354 'target_repo': PullRequest.target_repo_id
353 }
355 }
354 if order_dir == 'asc':
356 if order_dir == 'asc':
355 q = q.order_by(order_map[order_by].asc())
357 q = q.order_by(order_map[order_by].asc())
356 else:
358 else:
357 q = q.order_by(order_map[order_by].desc())
359 q = q.order_by(order_map[order_by].desc())
358
360
359 return q
361 return q
360
362
361 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
363 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
362 opened_by=None):
364 opened_by=None):
363 """
365 """
364 Count the number of pull requests for a specific repository.
366 Count the number of pull requests for a specific repository.
365
367
366 :param repo_name: target or source repo
368 :param repo_name: target or source repo
367 :param search_q: filter by text
369 :param search_q: filter by text
368 :param source: boolean flag to specify if repo_name refers to source
370 :param source: boolean flag to specify if repo_name refers to source
369 :param statuses: list of pull request statuses
371 :param statuses: list of pull request statuses
370 :param opened_by: author user of the pull request
372 :param opened_by: author user of the pull request
371 :returns: int number of pull requests
373 :returns: int number of pull requests
372 """
374 """
373 q = self._prepare_get_all_query(
375 q = self._prepare_get_all_query(
374 repo_name, search_q=search_q, source=source, statuses=statuses,
376 repo_name, search_q=search_q, source=source, statuses=statuses,
375 opened_by=opened_by)
377 opened_by=opened_by)
376
378
377 return q.count()
379 return q.count()
378
380
379 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
381 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
380 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
382 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
381 """
383 """
382 Get all pull requests for a specific repository.
384 Get all pull requests for a specific repository.
383
385
384 :param repo_name: target or source repo
386 :param repo_name: target or source repo
385 :param search_q: filter by text
387 :param search_q: filter by text
386 :param source: boolean flag to specify if repo_name refers to source
388 :param source: boolean flag to specify if repo_name refers to source
387 :param statuses: list of pull request statuses
389 :param statuses: list of pull request statuses
388 :param opened_by: author user of the pull request
390 :param opened_by: author user of the pull request
389 :param offset: pagination offset
391 :param offset: pagination offset
390 :param length: length of returned list
392 :param length: length of returned list
391 :param order_by: order of the returned list
393 :param order_by: order of the returned list
392 :param order_dir: 'asc' or 'desc' ordering direction
394 :param order_dir: 'asc' or 'desc' ordering direction
393 :returns: list of pull requests
395 :returns: list of pull requests
394 """
396 """
395 q = self._prepare_get_all_query(
397 q = self._prepare_get_all_query(
396 repo_name, search_q=search_q, source=source, statuses=statuses,
398 repo_name, search_q=search_q, source=source, statuses=statuses,
397 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
399 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
398
400
399 if length:
401 if length:
400 pull_requests = q.limit(length).offset(offset).all()
402 pull_requests = q.limit(length).offset(offset).all()
401 else:
403 else:
402 pull_requests = q.all()
404 pull_requests = q.all()
403
405
404 return pull_requests
406 return pull_requests
405
407
406 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
408 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
407 opened_by=None):
409 opened_by=None):
408 """
410 """
409 Count the number of pull requests for a specific repository that are
411 Count the number of pull requests for a specific repository that are
410 awaiting review.
412 awaiting review.
411
413
412 :param repo_name: target or source repo
414 :param repo_name: target or source repo
413 :param search_q: filter by text
415 :param search_q: filter by text
414 :param source: boolean flag to specify if repo_name refers to source
416 :param source: boolean flag to specify if repo_name refers to source
415 :param statuses: list of pull request statuses
417 :param statuses: list of pull request statuses
416 :param opened_by: author user of the pull request
418 :param opened_by: author user of the pull request
417 :returns: int number of pull requests
419 :returns: int number of pull requests
418 """
420 """
419 pull_requests = self.get_awaiting_review(
421 pull_requests = self.get_awaiting_review(
420 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
422 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
421
423
422 return len(pull_requests)
424 return len(pull_requests)
423
425
424 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
426 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
425 opened_by=None, offset=0, length=None,
427 opened_by=None, offset=0, length=None,
426 order_by=None, order_dir='desc'):
428 order_by=None, order_dir='desc'):
427 """
429 """
428 Get all pull requests for a specific repository that are awaiting
430 Get all pull requests for a specific repository that are awaiting
429 review.
431 review.
430
432
431 :param repo_name: target or source repo
433 :param repo_name: target or source repo
432 :param search_q: filter by text
434 :param search_q: filter by text
433 :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
434 :param statuses: list of pull request statuses
436 :param statuses: list of pull request statuses
435 :param opened_by: author user of the pull request
437 :param opened_by: author user of the pull request
436 :param offset: pagination offset
438 :param offset: pagination offset
437 :param length: length of returned list
439 :param length: length of returned list
438 :param order_by: order of the returned list
440 :param order_by: order of the returned list
439 :param order_dir: 'asc' or 'desc' ordering direction
441 :param order_dir: 'asc' or 'desc' ordering direction
440 :returns: list of pull requests
442 :returns: list of pull requests
441 """
443 """
442 pull_requests = self.get_all(
444 pull_requests = self.get_all(
443 repo_name, search_q=search_q, source=source, statuses=statuses,
445 repo_name, search_q=search_q, source=source, statuses=statuses,
444 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
446 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
445
447
446 _filtered_pull_requests = []
448 _filtered_pull_requests = []
447 for pr in pull_requests:
449 for pr in pull_requests:
448 status = pr.calculated_review_status()
450 status = pr.calculated_review_status()
449 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
451 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
450 ChangesetStatus.STATUS_UNDER_REVIEW]:
452 ChangesetStatus.STATUS_UNDER_REVIEW]:
451 _filtered_pull_requests.append(pr)
453 _filtered_pull_requests.append(pr)
452 if length:
454 if length:
453 return _filtered_pull_requests[offset:offset+length]
455 return _filtered_pull_requests[offset:offset+length]
454 else:
456 else:
455 return _filtered_pull_requests
457 return _filtered_pull_requests
456
458
457 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
459 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
458 opened_by=None, user_id=None):
460 opened_by=None, user_id=None):
459 """
461 """
460 Count the number of pull requests for a specific repository that are
462 Count the number of pull requests for a specific repository that are
461 awaiting review from a specific user.
463 awaiting review from a specific user.
462
464
463 :param repo_name: target or source repo
465 :param repo_name: target or source repo
464 :param search_q: filter by text
466 :param search_q: filter by text
465 :param source: boolean flag to specify if repo_name refers to source
467 :param source: boolean flag to specify if repo_name refers to source
466 :param statuses: list of pull request statuses
468 :param statuses: list of pull request statuses
467 :param opened_by: author user of the pull request
469 :param opened_by: author user of the pull request
468 :param user_id: reviewer user of the pull request
470 :param user_id: reviewer user of the pull request
469 :returns: int number of pull requests
471 :returns: int number of pull requests
470 """
472 """
471 pull_requests = self.get_awaiting_my_review(
473 pull_requests = self.get_awaiting_my_review(
472 repo_name, search_q=search_q, source=source, statuses=statuses,
474 repo_name, search_q=search_q, source=source, statuses=statuses,
473 opened_by=opened_by, user_id=user_id)
475 opened_by=opened_by, user_id=user_id)
474
476
475 return len(pull_requests)
477 return len(pull_requests)
476
478
477 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
479 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
478 opened_by=None, user_id=None, offset=0,
480 opened_by=None, user_id=None, offset=0,
479 length=None, order_by=None, order_dir='desc'):
481 length=None, order_by=None, order_dir='desc'):
480 """
482 """
481 Get all pull requests for a specific repository that are awaiting
483 Get all pull requests for a specific repository that are awaiting
482 review from a specific user.
484 review from a specific user.
483
485
484 :param repo_name: target or source repo
486 :param repo_name: target or source repo
485 :param search_q: filter by text
487 :param search_q: filter by text
486 :param source: boolean flag to specify if repo_name refers to source
488 :param source: boolean flag to specify if repo_name refers to source
487 :param statuses: list of pull request statuses
489 :param statuses: list of pull request statuses
488 :param opened_by: author user of the pull request
490 :param opened_by: author user of the pull request
489 :param user_id: reviewer user of the pull request
491 :param user_id: reviewer user of the pull request
490 :param offset: pagination offset
492 :param offset: pagination offset
491 :param length: length of returned list
493 :param length: length of returned list
492 :param order_by: order of the returned list
494 :param order_by: order of the returned list
493 :param order_dir: 'asc' or 'desc' ordering direction
495 :param order_dir: 'asc' or 'desc' ordering direction
494 :returns: list of pull requests
496 :returns: list of pull requests
495 """
497 """
496 pull_requests = self.get_all(
498 pull_requests = self.get_all(
497 repo_name, search_q=search_q, source=source, statuses=statuses,
499 repo_name, search_q=search_q, source=source, statuses=statuses,
498 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
500 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
499
501
500 _my = PullRequestModel().get_not_reviewed(user_id)
502 _my = PullRequestModel().get_not_reviewed(user_id)
501 my_participation = []
503 my_participation = []
502 for pr in pull_requests:
504 for pr in pull_requests:
503 if pr in _my:
505 if pr in _my:
504 my_participation.append(pr)
506 my_participation.append(pr)
505 _filtered_pull_requests = my_participation
507 _filtered_pull_requests = my_participation
506 if length:
508 if length:
507 return _filtered_pull_requests[offset:offset+length]
509 return _filtered_pull_requests[offset:offset+length]
508 else:
510 else:
509 return _filtered_pull_requests
511 return _filtered_pull_requests
510
512
511 def get_not_reviewed(self, user_id):
513 def get_not_reviewed(self, user_id):
512 return [
514 return [
513 x.pull_request for x in PullRequestReviewers.query().filter(
515 x.pull_request for x in PullRequestReviewers.query().filter(
514 PullRequestReviewers.user_id == user_id).all()
516 PullRequestReviewers.user_id == user_id).all()
515 ]
517 ]
516
518
517 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
519 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
518 order_by=None, order_dir='desc'):
520 order_by=None, order_dir='desc'):
519 q = PullRequest.query()
521 q = PullRequest.query()
520 if user_id:
522 if user_id:
521 reviewers_subquery = Session().query(
523 reviewers_subquery = Session().query(
522 PullRequestReviewers.pull_request_id).filter(
524 PullRequestReviewers.pull_request_id).filter(
523 PullRequestReviewers.user_id == user_id).subquery()
525 PullRequestReviewers.user_id == user_id).subquery()
524 user_filter = or_(
526 user_filter = or_(
525 PullRequest.user_id == user_id,
527 PullRequest.user_id == user_id,
526 PullRequest.pull_request_id.in_(reviewers_subquery)
528 PullRequest.pull_request_id.in_(reviewers_subquery)
527 )
529 )
528 q = PullRequest.query().filter(user_filter)
530 q = PullRequest.query().filter(user_filter)
529
531
530 # closed,opened
532 # closed,opened
531 if statuses:
533 if statuses:
532 q = q.filter(PullRequest.status.in_(statuses))
534 q = q.filter(PullRequest.status.in_(statuses))
533
535
534 if query:
536 if query:
535 like_expression = u'%{}%'.format(safe_unicode(query))
537 like_expression = u'%{}%'.format(safe_unicode(query))
536 q = q.join(User)
538 q = q.join(User)
537 q = q.filter(or_(
539 q = q.filter(or_(
538 cast(PullRequest.pull_request_id, String).ilike(like_expression),
540 cast(PullRequest.pull_request_id, String).ilike(like_expression),
539 User.username.ilike(like_expression),
541 User.username.ilike(like_expression),
540 PullRequest.title.ilike(like_expression),
542 PullRequest.title.ilike(like_expression),
541 PullRequest.description.ilike(like_expression),
543 PullRequest.description.ilike(like_expression),
542 ))
544 ))
543 if order_by:
545 if order_by:
544 order_map = {
546 order_map = {
545 'name_raw': PullRequest.pull_request_id,
547 'name_raw': PullRequest.pull_request_id,
546 'title': PullRequest.title,
548 'title': PullRequest.title,
547 'updated_on_raw': PullRequest.updated_on,
549 'updated_on_raw': PullRequest.updated_on,
548 'target_repo': PullRequest.target_repo_id
550 'target_repo': PullRequest.target_repo_id
549 }
551 }
550 if order_dir == 'asc':
552 if order_dir == 'asc':
551 q = q.order_by(order_map[order_by].asc())
553 q = q.order_by(order_map[order_by].asc())
552 else:
554 else:
553 q = q.order_by(order_map[order_by].desc())
555 q = q.order_by(order_map[order_by].desc())
554
556
555 return q
557 return q
556
558
557 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
559 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
558 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
560 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
559 return q.count()
561 return q.count()
560
562
561 def get_im_participating_in(
563 def get_im_participating_in(
562 self, user_id=None, statuses=None, query='', offset=0,
564 self, user_id=None, statuses=None, query='', offset=0,
563 length=None, order_by=None, order_dir='desc'):
565 length=None, order_by=None, order_dir='desc'):
564 """
566 """
565 Get all Pull requests that i'm participating in, or i have opened
567 Get all Pull requests that i'm participating in, or i have opened
566 """
568 """
567
569
568 q = self._prepare_participating_query(
570 q = self._prepare_participating_query(
569 user_id, statuses=statuses, query=query, order_by=order_by,
571 user_id, statuses=statuses, query=query, order_by=order_by,
570 order_dir=order_dir)
572 order_dir=order_dir)
571
573
572 if length:
574 if length:
573 pull_requests = q.limit(length).offset(offset).all()
575 pull_requests = q.limit(length).offset(offset).all()
574 else:
576 else:
575 pull_requests = q.all()
577 pull_requests = q.all()
576
578
577 return pull_requests
579 return pull_requests
578
580
579 def get_versions(self, pull_request):
581 def get_versions(self, pull_request):
580 """
582 """
581 returns version of pull request sorted by ID descending
583 returns version of pull request sorted by ID descending
582 """
584 """
583 return PullRequestVersion.query()\
585 return PullRequestVersion.query()\
584 .filter(PullRequestVersion.pull_request == pull_request)\
586 .filter(PullRequestVersion.pull_request == pull_request)\
585 .order_by(PullRequestVersion.pull_request_version_id.asc())\
587 .order_by(PullRequestVersion.pull_request_version_id.asc())\
586 .all()
588 .all()
587
589
588 def get_pr_version(self, pull_request_id, version=None):
590 def get_pr_version(self, pull_request_id, version=None):
589 at_version = None
591 at_version = None
590
592