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