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