##// END OF EJS Templates
events: fix bug, 'url' was not in dict
dan -
r392:b65452d3 default
parent child Browse files
Show More
@@ -1,71 +1,71 b''
1 # Copyright (C) 2016-2016 RhodeCode GmbH
1 # Copyright (C) 2016-2016 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 from datetime import datetime
19 from datetime import datetime
20 from marshmallow import Schema, fields
20 from marshmallow import Schema, fields
21 from pyramid.threadlocal import get_current_request
21 from pyramid.threadlocal import get_current_request
22 from rhodecode.lib.utils2 import AttributeDict
22 from rhodecode.lib.utils2 import AttributeDict
23
23
24
24
25 # this is a user object to be used for events caused by the system (eg. shell)
25 # this is a user object to be used for events caused by the system (eg. shell)
26 SYSTEM_USER = AttributeDict(dict(
26 SYSTEM_USER = AttributeDict(dict(
27 username='__SYSTEM__'
27 username='__SYSTEM__'
28 ))
28 ))
29
29
30
30
31 class UserSchema(Schema):
31 class UserSchema(Schema):
32 """
32 """
33 Marshmallow schema for a user
33 Marshmallow schema for a user
34 """
34 """
35 username = fields.Str()
35 username = fields.Str()
36
36
37
37
38 class RhodecodeEventSchema(Schema):
38 class RhodecodeEventSchema(Schema):
39 """
39 """
40 Marshmallow schema for a rhodecode event
40 Marshmallow schema for a rhodecode event
41 """
41 """
42 utc_timestamp = fields.DateTime()
42 utc_timestamp = fields.DateTime()
43 actor = fields.Nested(UserSchema)
43 actor = fields.Nested(UserSchema)
44 actor_ip = fields.Str()
44 actor_ip = fields.Str()
45 name = fields.Str()
45 name = fields.Str()
46
46
47
47
48 class RhodecodeEvent(object):
48 class RhodecodeEvent(object):
49 """
49 """
50 Base event class for all Rhodecode events
50 Base event class for all Rhodecode events
51 """
51 """
52 MarshmallowSchema = RhodecodeEventSchema
52 MarshmallowSchema = RhodecodeEventSchema
53
53
54 def __init__(self):
54 def __init__(self):
55 self.request = get_current_request()
55 self.request = get_current_request()
56 self.utc_timestamp = datetime.utcnow()
56 self.utc_timestamp = datetime.utcnow()
57
57
58 @property
58 @property
59 def actor(self):
59 def actor(self):
60 if self.request:
60 if self.request:
61 return self.request.user.get_instance()
61 return self.request.user.get_instance()
62 return SYSTEM_USER
62 return SYSTEM_USER
63
63
64 @property
64 @property
65 def actor_ip(self):
65 def actor_ip(self):
66 if self.request:
66 if self.request:
67 return self.request.user.ip_addr
67 return self.request.user.ip_addr
68 return '<no ip available>'
68 return '<no ip available>'
69
69
70 def as_dict(self):
70 def as_dict(self):
71 return self.MarshmallowSchema().dump(self).data No newline at end of file
71 return self.MarshmallowSchema().dump(self).data
@@ -1,1153 +1,1154 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.i18n.translation import lazy_ugettext
32 from pylons.i18n.translation import lazy_ugettext
33
33
34 import rhodecode
34 import rhodecode
35 from rhodecode.lib import helpers as h, hooks_utils, diffs
35 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib.compat import OrderedDict
36 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
37 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.markup_renderer import (
38 from rhodecode.lib.markup_renderer import (
39 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
39 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 from rhodecode.lib.utils import action_logger
40 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
41 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.vcs.backends.base import (
42 from rhodecode.lib.vcs.backends.base import (
43 Reference, MergeResponse, MergeFailureReason)
43 Reference, MergeResponse, MergeFailureReason)
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, EmptyRepositoryError)
45 CommitDoesNotExistError, EmptyRepositoryError)
46 from rhodecode.model import BaseModel
46 from rhodecode.model import BaseModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import ChangesetCommentsModel
48 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 PullRequest, PullRequestReviewers, Notification, ChangesetStatus,
50 PullRequest, PullRequestReviewers, Notification, ChangesetStatus,
51 PullRequestVersion, ChangesetComment)
51 PullRequestVersion, ChangesetComment)
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.notification import NotificationModel, \
53 from rhodecode.model.notification import NotificationModel, \
54 EmailNotificationModel
54 EmailNotificationModel
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56 from rhodecode.model.settings import VcsSettingsModel
56 from rhodecode.model.settings import VcsSettingsModel
57
57
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 class PullRequestModel(BaseModel):
62 class PullRequestModel(BaseModel):
63
63
64 cls = PullRequest
64 cls = PullRequest
65
65
66 DIFF_CONTEXT = 3
66 DIFF_CONTEXT = 3
67
67
68 MERGE_STATUS_MESSAGES = {
68 MERGE_STATUS_MESSAGES = {
69 MergeFailureReason.NONE: lazy_ugettext(
69 MergeFailureReason.NONE: lazy_ugettext(
70 'This pull request can be automatically merged.'),
70 'This pull request can be automatically merged.'),
71 MergeFailureReason.UNKNOWN: lazy_ugettext(
71 MergeFailureReason.UNKNOWN: lazy_ugettext(
72 'This pull request cannot be merged because of an unhandled'
72 'This pull request cannot be merged because of an unhandled'
73 ' exception.'),
73 ' exception.'),
74 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
74 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
75 'This pull request cannot be merged because of conflicts.'),
75 'This pull request cannot be merged because of conflicts.'),
76 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
76 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
77 'This pull request could not be merged because push to target'
77 'This pull request could not be merged because push to target'
78 ' failed.'),
78 ' failed.'),
79 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
79 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
80 'This pull request cannot be merged because the target is not a'
80 'This pull request cannot be merged because the target is not a'
81 ' head.'),
81 ' head.'),
82 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
82 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
83 'This pull request cannot be merged because the source contains'
83 'This pull request cannot be merged because the source contains'
84 ' more branches than the target.'),
84 ' more branches than the target.'),
85 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
85 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
86 'This pull request cannot be merged because the target has'
86 'This pull request cannot be merged because the target has'
87 ' multiple heads.'),
87 ' multiple heads.'),
88 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
88 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
89 'This pull request cannot be merged because the target repository'
89 'This pull request cannot be merged because the target repository'
90 ' is locked.'),
90 ' is locked.'),
91 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
91 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
92 'This pull request cannot be merged because the target or the '
92 'This pull request cannot be merged because the target or the '
93 'source reference is missing.'),
93 'source reference is missing.'),
94 }
94 }
95
95
96 def __get_pull_request(self, pull_request):
96 def __get_pull_request(self, pull_request):
97 return self._get_instance(PullRequest, pull_request)
97 return self._get_instance(PullRequest, pull_request)
98
98
99 def _check_perms(self, perms, pull_request, user, api=False):
99 def _check_perms(self, perms, pull_request, user, api=False):
100 if not api:
100 if not api:
101 return h.HasRepoPermissionAny(*perms)(
101 return h.HasRepoPermissionAny(*perms)(
102 user=user, repo_name=pull_request.target_repo.repo_name)
102 user=user, repo_name=pull_request.target_repo.repo_name)
103 else:
103 else:
104 return h.HasRepoPermissionAnyApi(*perms)(
104 return h.HasRepoPermissionAnyApi(*perms)(
105 user=user, repo_name=pull_request.target_repo.repo_name)
105 user=user, repo_name=pull_request.target_repo.repo_name)
106
106
107 def check_user_read(self, pull_request, user, api=False):
107 def check_user_read(self, pull_request, user, api=False):
108 _perms = ('repository.admin', 'repository.write', 'repository.read',)
108 _perms = ('repository.admin', 'repository.write', 'repository.read',)
109 return self._check_perms(_perms, pull_request, user, api)
109 return self._check_perms(_perms, pull_request, user, api)
110
110
111 def check_user_merge(self, pull_request, user, api=False):
111 def check_user_merge(self, pull_request, user, api=False):
112 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
112 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
113 return self._check_perms(_perms, pull_request, user, api)
113 return self._check_perms(_perms, pull_request, user, api)
114
114
115 def check_user_update(self, pull_request, user, api=False):
115 def check_user_update(self, pull_request, user, api=False):
116 owner = user.user_id == pull_request.user_id
116 owner = user.user_id == pull_request.user_id
117 return self.check_user_merge(pull_request, user, api) or owner
117 return self.check_user_merge(pull_request, user, api) or owner
118
118
119 def check_user_change_status(self, pull_request, user, api=False):
119 def check_user_change_status(self, pull_request, user, api=False):
120 reviewer = user.user_id in [x.user_id for x in
120 reviewer = user.user_id in [x.user_id for x in
121 pull_request.reviewers]
121 pull_request.reviewers]
122 return self.check_user_update(pull_request, user, api) or reviewer
122 return self.check_user_update(pull_request, user, api) or reviewer
123
123
124 def get(self, pull_request):
124 def get(self, pull_request):
125 return self.__get_pull_request(pull_request)
125 return self.__get_pull_request(pull_request)
126
126
127 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
127 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
128 opened_by=None, order_by=None,
128 opened_by=None, order_by=None,
129 order_dir='desc'):
129 order_dir='desc'):
130 repo = self._get_repo(repo_name)
130 repo = self._get_repo(repo_name)
131 q = PullRequest.query()
131 q = PullRequest.query()
132 # source or target
132 # source or target
133 if source:
133 if source:
134 q = q.filter(PullRequest.source_repo == repo)
134 q = q.filter(PullRequest.source_repo == repo)
135 else:
135 else:
136 q = q.filter(PullRequest.target_repo == repo)
136 q = q.filter(PullRequest.target_repo == repo)
137
137
138 # closed,opened
138 # closed,opened
139 if statuses:
139 if statuses:
140 q = q.filter(PullRequest.status.in_(statuses))
140 q = q.filter(PullRequest.status.in_(statuses))
141
141
142 # opened by filter
142 # opened by filter
143 if opened_by:
143 if opened_by:
144 q = q.filter(PullRequest.user_id.in_(opened_by))
144 q = q.filter(PullRequest.user_id.in_(opened_by))
145
145
146 if order_by:
146 if order_by:
147 order_map = {
147 order_map = {
148 'name_raw': PullRequest.pull_request_id,
148 'name_raw': PullRequest.pull_request_id,
149 'title': PullRequest.title,
149 'title': PullRequest.title,
150 'updated_on_raw': PullRequest.updated_on
150 'updated_on_raw': PullRequest.updated_on
151 }
151 }
152 if order_dir == 'asc':
152 if order_dir == 'asc':
153 q = q.order_by(order_map[order_by].asc())
153 q = q.order_by(order_map[order_by].asc())
154 else:
154 else:
155 q = q.order_by(order_map[order_by].desc())
155 q = q.order_by(order_map[order_by].desc())
156
156
157 return q
157 return q
158
158
159 def count_all(self, repo_name, source=False, statuses=None,
159 def count_all(self, repo_name, source=False, statuses=None,
160 opened_by=None):
160 opened_by=None):
161 """
161 """
162 Count the number of pull requests for a specific repository.
162 Count the number of pull requests for a specific repository.
163
163
164 :param repo_name: target or source repo
164 :param repo_name: target or source repo
165 :param source: boolean flag to specify if repo_name refers to source
165 :param source: boolean flag to specify if repo_name refers to source
166 :param statuses: list of pull request statuses
166 :param statuses: list of pull request statuses
167 :param opened_by: author user of the pull request
167 :param opened_by: author user of the pull request
168 :returns: int number of pull requests
168 :returns: int number of pull requests
169 """
169 """
170 q = self._prepare_get_all_query(
170 q = self._prepare_get_all_query(
171 repo_name, source=source, statuses=statuses, opened_by=opened_by)
171 repo_name, source=source, statuses=statuses, opened_by=opened_by)
172
172
173 return q.count()
173 return q.count()
174
174
175 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
175 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
176 offset=0, length=None, order_by=None, order_dir='desc'):
176 offset=0, length=None, order_by=None, order_dir='desc'):
177 """
177 """
178 Get all pull requests for a specific repository.
178 Get all pull requests for a specific repository.
179
179
180 :param repo_name: target or source repo
180 :param repo_name: target or source repo
181 :param source: boolean flag to specify if repo_name refers to source
181 :param source: boolean flag to specify if repo_name refers to source
182 :param statuses: list of pull request statuses
182 :param statuses: list of pull request statuses
183 :param opened_by: author user of the pull request
183 :param opened_by: author user of the pull request
184 :param offset: pagination offset
184 :param offset: pagination offset
185 :param length: length of returned list
185 :param length: length of returned list
186 :param order_by: order of the returned list
186 :param order_by: order of the returned list
187 :param order_dir: 'asc' or 'desc' ordering direction
187 :param order_dir: 'asc' or 'desc' ordering direction
188 :returns: list of pull requests
188 :returns: list of pull requests
189 """
189 """
190 q = self._prepare_get_all_query(
190 q = self._prepare_get_all_query(
191 repo_name, source=source, statuses=statuses, opened_by=opened_by,
191 repo_name, source=source, statuses=statuses, opened_by=opened_by,
192 order_by=order_by, order_dir=order_dir)
192 order_by=order_by, order_dir=order_dir)
193
193
194 if length:
194 if length:
195 pull_requests = q.limit(length).offset(offset).all()
195 pull_requests = q.limit(length).offset(offset).all()
196 else:
196 else:
197 pull_requests = q.all()
197 pull_requests = q.all()
198
198
199 return pull_requests
199 return pull_requests
200
200
201 def count_awaiting_review(self, repo_name, source=False, statuses=None,
201 def count_awaiting_review(self, repo_name, source=False, statuses=None,
202 opened_by=None):
202 opened_by=None):
203 """
203 """
204 Count the number of pull requests for a specific repository that are
204 Count the number of pull requests for a specific repository that are
205 awaiting review.
205 awaiting review.
206
206
207 :param repo_name: target or source repo
207 :param repo_name: target or source repo
208 :param source: boolean flag to specify if repo_name refers to source
208 :param source: boolean flag to specify if repo_name refers to source
209 :param statuses: list of pull request statuses
209 :param statuses: list of pull request statuses
210 :param opened_by: author user of the pull request
210 :param opened_by: author user of the pull request
211 :returns: int number of pull requests
211 :returns: int number of pull requests
212 """
212 """
213 pull_requests = self.get_awaiting_review(
213 pull_requests = self.get_awaiting_review(
214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
215
215
216 return len(pull_requests)
216 return len(pull_requests)
217
217
218 def get_awaiting_review(self, repo_name, source=False, statuses=None,
218 def get_awaiting_review(self, repo_name, source=False, statuses=None,
219 opened_by=None, offset=0, length=None,
219 opened_by=None, offset=0, length=None,
220 order_by=None, order_dir='desc'):
220 order_by=None, order_dir='desc'):
221 """
221 """
222 Get all pull requests for a specific repository that are awaiting
222 Get all pull requests for a specific repository that are awaiting
223 review.
223 review.
224
224
225 :param repo_name: target or source repo
225 :param repo_name: target or source repo
226 :param source: boolean flag to specify if repo_name refers to source
226 :param source: boolean flag to specify if repo_name refers to source
227 :param statuses: list of pull request statuses
227 :param statuses: list of pull request statuses
228 :param opened_by: author user of the pull request
228 :param opened_by: author user of the pull request
229 :param offset: pagination offset
229 :param offset: pagination offset
230 :param length: length of returned list
230 :param length: length of returned list
231 :param order_by: order of the returned list
231 :param order_by: order of the returned list
232 :param order_dir: 'asc' or 'desc' ordering direction
232 :param order_dir: 'asc' or 'desc' ordering direction
233 :returns: list of pull requests
233 :returns: list of pull requests
234 """
234 """
235 pull_requests = self.get_all(
235 pull_requests = self.get_all(
236 repo_name, source=source, statuses=statuses, opened_by=opened_by,
236 repo_name, source=source, statuses=statuses, opened_by=opened_by,
237 order_by=order_by, order_dir=order_dir)
237 order_by=order_by, order_dir=order_dir)
238
238
239 _filtered_pull_requests = []
239 _filtered_pull_requests = []
240 for pr in pull_requests:
240 for pr in pull_requests:
241 status = pr.calculated_review_status()
241 status = pr.calculated_review_status()
242 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
242 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
243 ChangesetStatus.STATUS_UNDER_REVIEW]:
243 ChangesetStatus.STATUS_UNDER_REVIEW]:
244 _filtered_pull_requests.append(pr)
244 _filtered_pull_requests.append(pr)
245 if length:
245 if length:
246 return _filtered_pull_requests[offset:offset+length]
246 return _filtered_pull_requests[offset:offset+length]
247 else:
247 else:
248 return _filtered_pull_requests
248 return _filtered_pull_requests
249
249
250 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
251 opened_by=None, user_id=None):
251 opened_by=None, user_id=None):
252 """
252 """
253 Count the number of pull requests for a specific repository that are
253 Count the number of pull requests for a specific repository that are
254 awaiting review from a specific user.
254 awaiting review from a specific user.
255
255
256 :param repo_name: target or source repo
256 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
257 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
260 :param user_id: reviewer user of the pull request
260 :param user_id: reviewer user of the pull request
261 :returns: int number of pull requests
261 :returns: int number of pull requests
262 """
262 """
263 pull_requests = self.get_awaiting_my_review(
263 pull_requests = self.get_awaiting_my_review(
264 repo_name, source=source, statuses=statuses, opened_by=opened_by,
264 repo_name, source=source, statuses=statuses, opened_by=opened_by,
265 user_id=user_id)
265 user_id=user_id)
266
266
267 return len(pull_requests)
267 return len(pull_requests)
268
268
269 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
269 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
270 opened_by=None, user_id=None, offset=0,
270 opened_by=None, user_id=None, offset=0,
271 length=None, order_by=None, order_dir='desc'):
271 length=None, order_by=None, order_dir='desc'):
272 """
272 """
273 Get all pull requests for a specific repository that are awaiting
273 Get all pull requests for a specific repository that are awaiting
274 review from a specific user.
274 review from a specific user.
275
275
276 :param repo_name: target or source repo
276 :param repo_name: target or source repo
277 :param source: boolean flag to specify if repo_name refers to source
277 :param source: boolean flag to specify if repo_name refers to source
278 :param statuses: list of pull request statuses
278 :param statuses: list of pull request statuses
279 :param opened_by: author user of the pull request
279 :param opened_by: author user of the pull request
280 :param user_id: reviewer user of the pull request
280 :param user_id: reviewer user of the pull request
281 :param offset: pagination offset
281 :param offset: pagination offset
282 :param length: length of returned list
282 :param length: length of returned list
283 :param order_by: order of the returned list
283 :param order_by: order of the returned list
284 :param order_dir: 'asc' or 'desc' ordering direction
284 :param order_dir: 'asc' or 'desc' ordering direction
285 :returns: list of pull requests
285 :returns: list of pull requests
286 """
286 """
287 pull_requests = self.get_all(
287 pull_requests = self.get_all(
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 order_by=order_by, order_dir=order_dir)
289 order_by=order_by, order_dir=order_dir)
290
290
291 _my = PullRequestModel().get_not_reviewed(user_id)
291 _my = PullRequestModel().get_not_reviewed(user_id)
292 my_participation = []
292 my_participation = []
293 for pr in pull_requests:
293 for pr in pull_requests:
294 if pr in _my:
294 if pr in _my:
295 my_participation.append(pr)
295 my_participation.append(pr)
296 _filtered_pull_requests = my_participation
296 _filtered_pull_requests = my_participation
297 if length:
297 if length:
298 return _filtered_pull_requests[offset:offset+length]
298 return _filtered_pull_requests[offset:offset+length]
299 else:
299 else:
300 return _filtered_pull_requests
300 return _filtered_pull_requests
301
301
302 def get_not_reviewed(self, user_id):
302 def get_not_reviewed(self, user_id):
303 return [
303 return [
304 x.pull_request for x in PullRequestReviewers.query().filter(
304 x.pull_request for x in PullRequestReviewers.query().filter(
305 PullRequestReviewers.user_id == user_id).all()
305 PullRequestReviewers.user_id == user_id).all()
306 ]
306 ]
307
307
308 def get_versions(self, pull_request):
308 def get_versions(self, pull_request):
309 """
309 """
310 returns version of pull request sorted by ID descending
310 returns version of pull request sorted by ID descending
311 """
311 """
312 return PullRequestVersion.query()\
312 return PullRequestVersion.query()\
313 .filter(PullRequestVersion.pull_request == pull_request)\
313 .filter(PullRequestVersion.pull_request == pull_request)\
314 .order_by(PullRequestVersion.pull_request_version_id.asc())\
314 .order_by(PullRequestVersion.pull_request_version_id.asc())\
315 .all()
315 .all()
316
316
317 def create(self, created_by, source_repo, source_ref, target_repo,
317 def create(self, created_by, source_repo, source_ref, target_repo,
318 target_ref, revisions, reviewers, title, description=None):
318 target_ref, revisions, reviewers, title, description=None):
319 created_by_user = self._get_user(created_by)
319 created_by_user = self._get_user(created_by)
320 source_repo = self._get_repo(source_repo)
320 source_repo = self._get_repo(source_repo)
321 target_repo = self._get_repo(target_repo)
321 target_repo = self._get_repo(target_repo)
322
322
323 pull_request = PullRequest()
323 pull_request = PullRequest()
324 pull_request.source_repo = source_repo
324 pull_request.source_repo = source_repo
325 pull_request.source_ref = source_ref
325 pull_request.source_ref = source_ref
326 pull_request.target_repo = target_repo
326 pull_request.target_repo = target_repo
327 pull_request.target_ref = target_ref
327 pull_request.target_ref = target_ref
328 pull_request.revisions = revisions
328 pull_request.revisions = revisions
329 pull_request.title = title
329 pull_request.title = title
330 pull_request.description = description
330 pull_request.description = description
331 pull_request.author = created_by_user
331 pull_request.author = created_by_user
332
332
333 Session().add(pull_request)
333 Session().add(pull_request)
334 Session().flush()
334 Session().flush()
335
335
336 # members / reviewers
336 # members / reviewers
337 for user_id in set(reviewers):
337 for user_id in set(reviewers):
338 user = self._get_user(user_id)
338 user = self._get_user(user_id)
339 reviewer = PullRequestReviewers(user, pull_request)
339 reviewer = PullRequestReviewers(user, pull_request)
340 Session().add(reviewer)
340 Session().add(reviewer)
341
341
342 # Set approval status to "Under Review" for all commits which are
342 # Set approval status to "Under Review" for all commits which are
343 # part of this pull request.
343 # part of this pull request.
344 ChangesetStatusModel().set_status(
344 ChangesetStatusModel().set_status(
345 repo=target_repo,
345 repo=target_repo,
346 status=ChangesetStatus.STATUS_UNDER_REVIEW,
346 status=ChangesetStatus.STATUS_UNDER_REVIEW,
347 user=created_by_user,
347 user=created_by_user,
348 pull_request=pull_request
348 pull_request=pull_request
349 )
349 )
350
350
351 self.notify_reviewers(pull_request, reviewers)
351 self.notify_reviewers(pull_request, reviewers)
352 self._trigger_pull_request_hook(
352 self._trigger_pull_request_hook(
353 pull_request, created_by_user, 'create')
353 pull_request, created_by_user, 'create')
354
354
355 return pull_request
355 return pull_request
356
356
357 def _trigger_pull_request_hook(self, pull_request, user, action):
357 def _trigger_pull_request_hook(self, pull_request, user, action):
358 pull_request = self.__get_pull_request(pull_request)
358 pull_request = self.__get_pull_request(pull_request)
359 target_scm = pull_request.target_repo.scm_instance()
359 target_scm = pull_request.target_repo.scm_instance()
360 if action == 'create':
360 if action == 'create':
361 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
361 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
362 elif action == 'merge':
362 elif action == 'merge':
363 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
363 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
364 elif action == 'close':
364 elif action == 'close':
365 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
365 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
366 elif action == 'review_status_change':
366 elif action == 'review_status_change':
367 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
367 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
368 elif action == 'update':
368 elif action == 'update':
369 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
369 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
370 else:
370 else:
371 return
371 return
372
372
373 trigger_hook(
373 trigger_hook(
374 username=user.username,
374 username=user.username,
375 repo_name=pull_request.target_repo.repo_name,
375 repo_name=pull_request.target_repo.repo_name,
376 repo_alias=target_scm.alias,
376 repo_alias=target_scm.alias,
377 pull_request=pull_request)
377 pull_request=pull_request)
378
378
379 def _get_commit_ids(self, pull_request):
379 def _get_commit_ids(self, pull_request):
380 """
380 """
381 Return the commit ids of the merged pull request.
381 Return the commit ids of the merged pull request.
382
382
383 This method is not dealing correctly yet with the lack of autoupdates
383 This method is not dealing correctly yet with the lack of autoupdates
384 nor with the implicit target updates.
384 nor with the implicit target updates.
385 For example: if a commit in the source repo is already in the target it
385 For example: if a commit in the source repo is already in the target it
386 will be reported anyways.
386 will be reported anyways.
387 """
387 """
388 merge_rev = pull_request.merge_rev
388 merge_rev = pull_request.merge_rev
389 if merge_rev is None:
389 if merge_rev is None:
390 raise ValueError('This pull request was not merged yet')
390 raise ValueError('This pull request was not merged yet')
391
391
392 commit_ids = list(pull_request.revisions)
392 commit_ids = list(pull_request.revisions)
393 if merge_rev not in commit_ids:
393 if merge_rev not in commit_ids:
394 commit_ids.append(merge_rev)
394 commit_ids.append(merge_rev)
395
395
396 return commit_ids
396 return commit_ids
397
397
398 def merge(self, pull_request, user, extras):
398 def merge(self, pull_request, user, extras):
399 log.debug("Merging pull request %s", pull_request.pull_request_id)
399 log.debug("Merging pull request %s", pull_request.pull_request_id)
400 merge_state = self._merge_pull_request(pull_request, user, extras)
400 merge_state = self._merge_pull_request(pull_request, user, extras)
401 if merge_state.executed:
401 if merge_state.executed:
402 log.debug(
402 log.debug(
403 "Merge was successful, updating the pull request comments.")
403 "Merge was successful, updating the pull request comments.")
404 self._comment_and_close_pr(pull_request, user, merge_state)
404 self._comment_and_close_pr(pull_request, user, merge_state)
405 self._log_action('user_merged_pull_request', user, pull_request)
405 self._log_action('user_merged_pull_request', user, pull_request)
406 else:
406 else:
407 log.warn("Merge failed, not updating the pull request.")
407 log.warn("Merge failed, not updating the pull request.")
408 return merge_state
408 return merge_state
409
409
410 def _merge_pull_request(self, pull_request, user, extras):
410 def _merge_pull_request(self, pull_request, user, extras):
411 target_vcs = pull_request.target_repo.scm_instance()
411 target_vcs = pull_request.target_repo.scm_instance()
412 source_vcs = pull_request.source_repo.scm_instance()
412 source_vcs = pull_request.source_repo.scm_instance()
413 target_ref = self._refresh_reference(
413 target_ref = self._refresh_reference(
414 pull_request.target_ref_parts, target_vcs)
414 pull_request.target_ref_parts, target_vcs)
415
415
416 message = _(
416 message = _(
417 'Merge pull request #%(pr_id)s from '
417 'Merge pull request #%(pr_id)s from '
418 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
418 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
419 'pr_id': pull_request.pull_request_id,
419 'pr_id': pull_request.pull_request_id,
420 'source_repo': source_vcs.name,
420 'source_repo': source_vcs.name,
421 'source_ref_name': pull_request.source_ref_parts.name,
421 'source_ref_name': pull_request.source_ref_parts.name,
422 'pr_title': pull_request.title
422 'pr_title': pull_request.title
423 }
423 }
424
424
425 workspace_id = self._workspace_id(pull_request)
425 workspace_id = self._workspace_id(pull_request)
426 protocol = rhodecode.CONFIG.get('vcs.hooks.protocol')
426 protocol = rhodecode.CONFIG.get('vcs.hooks.protocol')
427 use_direct_calls = rhodecode.CONFIG.get('vcs.hooks.direct_calls')
427 use_direct_calls = rhodecode.CONFIG.get('vcs.hooks.direct_calls')
428 use_rebase = self._use_rebase_for_merging(pull_request)
428 use_rebase = self._use_rebase_for_merging(pull_request)
429
429
430 callback_daemon, extras = prepare_callback_daemon(
430 callback_daemon, extras = prepare_callback_daemon(
431 extras, protocol=protocol, use_direct_calls=use_direct_calls)
431 extras, protocol=protocol, use_direct_calls=use_direct_calls)
432
432
433 with callback_daemon:
433 with callback_daemon:
434 # TODO: johbo: Implement a clean way to run a config_override
434 # TODO: johbo: Implement a clean way to run a config_override
435 # for a single call.
435 # for a single call.
436 target_vcs.config.set(
436 target_vcs.config.set(
437 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
437 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
438 merge_state = target_vcs.merge(
438 merge_state = target_vcs.merge(
439 target_ref, source_vcs, pull_request.source_ref_parts,
439 target_ref, source_vcs, pull_request.source_ref_parts,
440 workspace_id, user_name=user.username,
440 workspace_id, user_name=user.username,
441 user_email=user.email, message=message, use_rebase=use_rebase)
441 user_email=user.email, message=message, use_rebase=use_rebase)
442 return merge_state
442 return merge_state
443
443
444 def _comment_and_close_pr(self, pull_request, user, merge_state):
444 def _comment_and_close_pr(self, pull_request, user, merge_state):
445 pull_request.merge_rev = merge_state.merge_commit_id
445 pull_request.merge_rev = merge_state.merge_commit_id
446 pull_request.updated_on = datetime.datetime.now()
446 pull_request.updated_on = datetime.datetime.now()
447
447
448 ChangesetCommentsModel().create(
448 ChangesetCommentsModel().create(
449 text=unicode(_('Pull request merged and closed')),
449 text=unicode(_('Pull request merged and closed')),
450 repo=pull_request.target_repo.repo_id,
450 repo=pull_request.target_repo.repo_id,
451 user=user.user_id,
451 user=user.user_id,
452 pull_request=pull_request.pull_request_id,
452 pull_request=pull_request.pull_request_id,
453 f_path=None,
453 f_path=None,
454 line_no=None,
454 line_no=None,
455 closing_pr=True
455 closing_pr=True
456 )
456 )
457
457
458 Session().add(pull_request)
458 Session().add(pull_request)
459 Session().flush()
459 Session().flush()
460 # TODO: paris: replace invalidation with less radical solution
460 # TODO: paris: replace invalidation with less radical solution
461 ScmModel().mark_for_invalidation(
461 ScmModel().mark_for_invalidation(
462 pull_request.target_repo.repo_name)
462 pull_request.target_repo.repo_name)
463 self._trigger_pull_request_hook(pull_request, user, 'merge')
463 self._trigger_pull_request_hook(pull_request, user, 'merge')
464
464
465 def has_valid_update_type(self, pull_request):
465 def has_valid_update_type(self, pull_request):
466 source_ref_type = pull_request.source_ref_parts.type
466 source_ref_type = pull_request.source_ref_parts.type
467 return source_ref_type in ['book', 'branch', 'tag']
467 return source_ref_type in ['book', 'branch', 'tag']
468
468
469 def update_commits(self, pull_request):
469 def update_commits(self, pull_request):
470 """
470 """
471 Get the updated list of commits for the pull request
471 Get the updated list of commits for the pull request
472 and return the new pull request version and the list
472 and return the new pull request version and the list
473 of commits processed by this update action
473 of commits processed by this update action
474 """
474 """
475
475
476 pull_request = self.__get_pull_request(pull_request)
476 pull_request = self.__get_pull_request(pull_request)
477 source_ref_type = pull_request.source_ref_parts.type
477 source_ref_type = pull_request.source_ref_parts.type
478 source_ref_name = pull_request.source_ref_parts.name
478 source_ref_name = pull_request.source_ref_parts.name
479 source_ref_id = pull_request.source_ref_parts.commit_id
479 source_ref_id = pull_request.source_ref_parts.commit_id
480
480
481 if not self.has_valid_update_type(pull_request):
481 if not self.has_valid_update_type(pull_request):
482 log.debug(
482 log.debug(
483 "Skipping update of pull request %s due to ref type: %s",
483 "Skipping update of pull request %s due to ref type: %s",
484 pull_request, source_ref_type)
484 pull_request, source_ref_type)
485 return (None, None)
485 return (None, None)
486
486
487 source_repo = pull_request.source_repo.scm_instance()
487 source_repo = pull_request.source_repo.scm_instance()
488 source_commit = source_repo.get_commit(commit_id=source_ref_name)
488 source_commit = source_repo.get_commit(commit_id=source_ref_name)
489 if source_ref_id == source_commit.raw_id:
489 if source_ref_id == source_commit.raw_id:
490 log.debug("Nothing changed in pull request %s", pull_request)
490 log.debug("Nothing changed in pull request %s", pull_request)
491 return (None, None)
491 return (None, None)
492
492
493 # Finally there is a need for an update
493 # Finally there is a need for an update
494 pull_request_version = self._create_version_from_snapshot(pull_request)
494 pull_request_version = self._create_version_from_snapshot(pull_request)
495 self._link_comments_to_version(pull_request_version)
495 self._link_comments_to_version(pull_request_version)
496
496
497 target_ref_type = pull_request.target_ref_parts.type
497 target_ref_type = pull_request.target_ref_parts.type
498 target_ref_name = pull_request.target_ref_parts.name
498 target_ref_name = pull_request.target_ref_parts.name
499 target_ref_id = pull_request.target_ref_parts.commit_id
499 target_ref_id = pull_request.target_ref_parts.commit_id
500 target_repo = pull_request.target_repo.scm_instance()
500 target_repo = pull_request.target_repo.scm_instance()
501
501
502 if target_ref_type in ('tag', 'branch', 'book'):
502 if target_ref_type in ('tag', 'branch', 'book'):
503 target_commit = target_repo.get_commit(target_ref_name)
503 target_commit = target_repo.get_commit(target_ref_name)
504 else:
504 else:
505 target_commit = target_repo.get_commit(target_ref_id)
505 target_commit = target_repo.get_commit(target_ref_id)
506
506
507 # re-compute commit ids
507 # re-compute commit ids
508 old_commit_ids = set(pull_request.revisions)
508 old_commit_ids = set(pull_request.revisions)
509 pre_load = ["author", "branch", "date", "message"]
509 pre_load = ["author", "branch", "date", "message"]
510 commit_ranges = target_repo.compare(
510 commit_ranges = target_repo.compare(
511 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
511 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
512 pre_load=pre_load)
512 pre_load=pre_load)
513
513
514 ancestor = target_repo.get_common_ancestor(
514 ancestor = target_repo.get_common_ancestor(
515 target_commit.raw_id, source_commit.raw_id, source_repo)
515 target_commit.raw_id, source_commit.raw_id, source_repo)
516
516
517 pull_request.source_ref = '%s:%s:%s' % (
517 pull_request.source_ref = '%s:%s:%s' % (
518 source_ref_type, source_ref_name, source_commit.raw_id)
518 source_ref_type, source_ref_name, source_commit.raw_id)
519 pull_request.target_ref = '%s:%s:%s' % (
519 pull_request.target_ref = '%s:%s:%s' % (
520 target_ref_type, target_ref_name, ancestor)
520 target_ref_type, target_ref_name, ancestor)
521 pull_request.revisions = [
521 pull_request.revisions = [
522 commit.raw_id for commit in reversed(commit_ranges)]
522 commit.raw_id for commit in reversed(commit_ranges)]
523 pull_request.updated_on = datetime.datetime.now()
523 pull_request.updated_on = datetime.datetime.now()
524 Session().add(pull_request)
524 Session().add(pull_request)
525 new_commit_ids = set(pull_request.revisions)
525 new_commit_ids = set(pull_request.revisions)
526
526
527 changes = self._calculate_commit_id_changes(
527 changes = self._calculate_commit_id_changes(
528 old_commit_ids, new_commit_ids)
528 old_commit_ids, new_commit_ids)
529
529
530 old_diff_data, new_diff_data = self._generate_update_diffs(
530 old_diff_data, new_diff_data = self._generate_update_diffs(
531 pull_request, pull_request_version)
531 pull_request, pull_request_version)
532
532
533 ChangesetCommentsModel().outdate_comments(
533 ChangesetCommentsModel().outdate_comments(
534 pull_request, old_diff_data=old_diff_data,
534 pull_request, old_diff_data=old_diff_data,
535 new_diff_data=new_diff_data)
535 new_diff_data=new_diff_data)
536
536
537 file_changes = self._calculate_file_changes(
537 file_changes = self._calculate_file_changes(
538 old_diff_data, new_diff_data)
538 old_diff_data, new_diff_data)
539
539
540 # Add an automatic comment to the pull request
540 # Add an automatic comment to the pull request
541 update_comment = ChangesetCommentsModel().create(
541 update_comment = ChangesetCommentsModel().create(
542 text=self._render_update_message(changes, file_changes),
542 text=self._render_update_message(changes, file_changes),
543 repo=pull_request.target_repo,
543 repo=pull_request.target_repo,
544 user=pull_request.author,
544 user=pull_request.author,
545 pull_request=pull_request,
545 pull_request=pull_request,
546 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
546 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
547
547
548 # Update status to "Under Review" for added commits
548 # Update status to "Under Review" for added commits
549 for commit_id in changes.added:
549 for commit_id in changes.added:
550 ChangesetStatusModel().set_status(
550 ChangesetStatusModel().set_status(
551 repo=pull_request.source_repo,
551 repo=pull_request.source_repo,
552 status=ChangesetStatus.STATUS_UNDER_REVIEW,
552 status=ChangesetStatus.STATUS_UNDER_REVIEW,
553 comment=update_comment,
553 comment=update_comment,
554 user=pull_request.author,
554 user=pull_request.author,
555 pull_request=pull_request,
555 pull_request=pull_request,
556 revision=commit_id)
556 revision=commit_id)
557
557
558 log.debug(
558 log.debug(
559 'Updated pull request %s, added_ids: %s, common_ids: %s, '
559 'Updated pull request %s, added_ids: %s, common_ids: %s, '
560 'removed_ids: %s', pull_request.pull_request_id,
560 'removed_ids: %s', pull_request.pull_request_id,
561 changes.added, changes.common, changes.removed)
561 changes.added, changes.common, changes.removed)
562 log.debug('Updated pull request with the following file changes: %s',
562 log.debug('Updated pull request with the following file changes: %s',
563 file_changes)
563 file_changes)
564
564
565 log.info(
565 log.info(
566 "Updated pull request %s from commit %s to commit %s, "
566 "Updated pull request %s from commit %s to commit %s, "
567 "stored new version %s of this pull request.",
567 "stored new version %s of this pull request.",
568 pull_request.pull_request_id, source_ref_id,
568 pull_request.pull_request_id, source_ref_id,
569 pull_request.source_ref_parts.commit_id,
569 pull_request.source_ref_parts.commit_id,
570 pull_request_version.pull_request_version_id)
570 pull_request_version.pull_request_version_id)
571 Session().commit()
571 Session().commit()
572 self._trigger_pull_request_hook(pull_request, pull_request.author,
572 self._trigger_pull_request_hook(pull_request, pull_request.author,
573 'update')
573 'update')
574 return (pull_request_version, changes)
574 return (pull_request_version, changes)
575
575
576 def _create_version_from_snapshot(self, pull_request):
576 def _create_version_from_snapshot(self, pull_request):
577 version = PullRequestVersion()
577 version = PullRequestVersion()
578 version.title = pull_request.title
578 version.title = pull_request.title
579 version.description = pull_request.description
579 version.description = pull_request.description
580 version.status = pull_request.status
580 version.status = pull_request.status
581 version.created_on = pull_request.created_on
581 version.created_on = pull_request.created_on
582 version.updated_on = pull_request.updated_on
582 version.updated_on = pull_request.updated_on
583 version.user_id = pull_request.user_id
583 version.user_id = pull_request.user_id
584 version.source_repo = pull_request.source_repo
584 version.source_repo = pull_request.source_repo
585 version.source_ref = pull_request.source_ref
585 version.source_ref = pull_request.source_ref
586 version.target_repo = pull_request.target_repo
586 version.target_repo = pull_request.target_repo
587 version.target_ref = pull_request.target_ref
587 version.target_ref = pull_request.target_ref
588
588
589 version._last_merge_source_rev = pull_request._last_merge_source_rev
589 version._last_merge_source_rev = pull_request._last_merge_source_rev
590 version._last_merge_target_rev = pull_request._last_merge_target_rev
590 version._last_merge_target_rev = pull_request._last_merge_target_rev
591 version._last_merge_status = pull_request._last_merge_status
591 version._last_merge_status = pull_request._last_merge_status
592 version.merge_rev = pull_request.merge_rev
592 version.merge_rev = pull_request.merge_rev
593
593
594 version.revisions = pull_request.revisions
594 version.revisions = pull_request.revisions
595 version.pull_request = pull_request
595 version.pull_request = pull_request
596 Session().add(version)
596 Session().add(version)
597 Session().flush()
597 Session().flush()
598
598
599 return version
599 return version
600
600
601 def _generate_update_diffs(self, pull_request, pull_request_version):
601 def _generate_update_diffs(self, pull_request, pull_request_version):
602 diff_context = (
602 diff_context = (
603 self.DIFF_CONTEXT +
603 self.DIFF_CONTEXT +
604 ChangesetCommentsModel.needed_extra_diff_context())
604 ChangesetCommentsModel.needed_extra_diff_context())
605 old_diff = self._get_diff_from_pr_or_version(
605 old_diff = self._get_diff_from_pr_or_version(
606 pull_request_version, context=diff_context)
606 pull_request_version, context=diff_context)
607 new_diff = self._get_diff_from_pr_or_version(
607 new_diff = self._get_diff_from_pr_or_version(
608 pull_request, context=diff_context)
608 pull_request, context=diff_context)
609
609
610 old_diff_data = diffs.DiffProcessor(old_diff)
610 old_diff_data = diffs.DiffProcessor(old_diff)
611 old_diff_data.prepare()
611 old_diff_data.prepare()
612 new_diff_data = diffs.DiffProcessor(new_diff)
612 new_diff_data = diffs.DiffProcessor(new_diff)
613 new_diff_data.prepare()
613 new_diff_data.prepare()
614
614
615 return old_diff_data, new_diff_data
615 return old_diff_data, new_diff_data
616
616
617 def _link_comments_to_version(self, pull_request_version):
617 def _link_comments_to_version(self, pull_request_version):
618 """
618 """
619 Link all unlinked comments of this pull request to the given version.
619 Link all unlinked comments of this pull request to the given version.
620
620
621 :param pull_request_version: The `PullRequestVersion` to which
621 :param pull_request_version: The `PullRequestVersion` to which
622 the comments shall be linked.
622 the comments shall be linked.
623
623
624 """
624 """
625 pull_request = pull_request_version.pull_request
625 pull_request = pull_request_version.pull_request
626 comments = ChangesetComment.query().filter(
626 comments = ChangesetComment.query().filter(
627 # TODO: johbo: Should we query for the repo at all here?
627 # TODO: johbo: Should we query for the repo at all here?
628 # Pending decision on how comments of PRs are to be related
628 # Pending decision on how comments of PRs are to be related
629 # to either the source repo, the target repo or no repo at all.
629 # to either the source repo, the target repo or no repo at all.
630 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
630 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
631 ChangesetComment.pull_request == pull_request,
631 ChangesetComment.pull_request == pull_request,
632 ChangesetComment.pull_request_version == None)
632 ChangesetComment.pull_request_version == None)
633
633
634 # TODO: johbo: Find out why this breaks if it is done in a bulk
634 # TODO: johbo: Find out why this breaks if it is done in a bulk
635 # operation.
635 # operation.
636 for comment in comments:
636 for comment in comments:
637 comment.pull_request_version_id = (
637 comment.pull_request_version_id = (
638 pull_request_version.pull_request_version_id)
638 pull_request_version.pull_request_version_id)
639 Session().add(comment)
639 Session().add(comment)
640
640
641 def _calculate_commit_id_changes(self, old_ids, new_ids):
641 def _calculate_commit_id_changes(self, old_ids, new_ids):
642 added = new_ids.difference(old_ids)
642 added = new_ids.difference(old_ids)
643 common = old_ids.intersection(new_ids)
643 common = old_ids.intersection(new_ids)
644 removed = old_ids.difference(new_ids)
644 removed = old_ids.difference(new_ids)
645 return ChangeTuple(added, common, removed)
645 return ChangeTuple(added, common, removed)
646
646
647 def _calculate_file_changes(self, old_diff_data, new_diff_data):
647 def _calculate_file_changes(self, old_diff_data, new_diff_data):
648
648
649 old_files = OrderedDict()
649 old_files = OrderedDict()
650 for diff_data in old_diff_data.parsed_diff:
650 for diff_data in old_diff_data.parsed_diff:
651 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
651 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
652
652
653 added_files = []
653 added_files = []
654 modified_files = []
654 modified_files = []
655 removed_files = []
655 removed_files = []
656 for diff_data in new_diff_data.parsed_diff:
656 for diff_data in new_diff_data.parsed_diff:
657 new_filename = diff_data['filename']
657 new_filename = diff_data['filename']
658 new_hash = md5_safe(diff_data['raw_diff'])
658 new_hash = md5_safe(diff_data['raw_diff'])
659
659
660 old_hash = old_files.get(new_filename)
660 old_hash = old_files.get(new_filename)
661 if not old_hash:
661 if not old_hash:
662 # file is not present in old diff, means it's added
662 # file is not present in old diff, means it's added
663 added_files.append(new_filename)
663 added_files.append(new_filename)
664 else:
664 else:
665 if new_hash != old_hash:
665 if new_hash != old_hash:
666 modified_files.append(new_filename)
666 modified_files.append(new_filename)
667 # now remove a file from old, since we have seen it already
667 # now remove a file from old, since we have seen it already
668 del old_files[new_filename]
668 del old_files[new_filename]
669
669
670 # removed files is when there are present in old, but not in NEW,
670 # removed files is when there are present in old, but not in NEW,
671 # since we remove old files that are present in new diff, left-overs
671 # since we remove old files that are present in new diff, left-overs
672 # if any should be the removed files
672 # if any should be the removed files
673 removed_files.extend(old_files.keys())
673 removed_files.extend(old_files.keys())
674
674
675 return FileChangeTuple(added_files, modified_files, removed_files)
675 return FileChangeTuple(added_files, modified_files, removed_files)
676
676
677 def _render_update_message(self, changes, file_changes):
677 def _render_update_message(self, changes, file_changes):
678 """
678 """
679 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
679 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
680 so it's always looking the same disregarding on which default
680 so it's always looking the same disregarding on which default
681 renderer system is using.
681 renderer system is using.
682
682
683 :param changes: changes named tuple
683 :param changes: changes named tuple
684 :param file_changes: file changes named tuple
684 :param file_changes: file changes named tuple
685
685
686 """
686 """
687 new_status = ChangesetStatus.get_status_lbl(
687 new_status = ChangesetStatus.get_status_lbl(
688 ChangesetStatus.STATUS_UNDER_REVIEW)
688 ChangesetStatus.STATUS_UNDER_REVIEW)
689
689
690 changed_files = (
690 changed_files = (
691 file_changes.added + file_changes.modified + file_changes.removed)
691 file_changes.added + file_changes.modified + file_changes.removed)
692
692
693 params = {
693 params = {
694 'under_review_label': new_status,
694 'under_review_label': new_status,
695 'added_commits': changes.added,
695 'added_commits': changes.added,
696 'removed_commits': changes.removed,
696 'removed_commits': changes.removed,
697 'changed_files': changed_files,
697 'changed_files': changed_files,
698 'added_files': file_changes.added,
698 'added_files': file_changes.added,
699 'modified_files': file_changes.modified,
699 'modified_files': file_changes.modified,
700 'removed_files': file_changes.removed,
700 'removed_files': file_changes.removed,
701 }
701 }
702 renderer = RstTemplateRenderer()
702 renderer = RstTemplateRenderer()
703 return renderer.render('pull_request_update.mako', **params)
703 return renderer.render('pull_request_update.mako', **params)
704
704
705 def edit(self, pull_request, title, description):
705 def edit(self, pull_request, title, description):
706 pull_request = self.__get_pull_request(pull_request)
706 pull_request = self.__get_pull_request(pull_request)
707 if pull_request.is_closed():
707 if pull_request.is_closed():
708 raise ValueError('This pull request is closed')
708 raise ValueError('This pull request is closed')
709 if title:
709 if title:
710 pull_request.title = title
710 pull_request.title = title
711 pull_request.description = description
711 pull_request.description = description
712 pull_request.updated_on = datetime.datetime.now()
712 pull_request.updated_on = datetime.datetime.now()
713 Session().add(pull_request)
713 Session().add(pull_request)
714
714
715 def update_reviewers(self, pull_request, reviewers_ids):
715 def update_reviewers(self, pull_request, reviewers_ids):
716 reviewers_ids = set(reviewers_ids)
716 reviewers_ids = set(reviewers_ids)
717 pull_request = self.__get_pull_request(pull_request)
717 pull_request = self.__get_pull_request(pull_request)
718 current_reviewers = PullRequestReviewers.query()\
718 current_reviewers = PullRequestReviewers.query()\
719 .filter(PullRequestReviewers.pull_request ==
719 .filter(PullRequestReviewers.pull_request ==
720 pull_request).all()
720 pull_request).all()
721 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
721 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
722
722
723 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
723 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
724 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
724 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
725
725
726 log.debug("Adding %s reviewers", ids_to_add)
726 log.debug("Adding %s reviewers", ids_to_add)
727 log.debug("Removing %s reviewers", ids_to_remove)
727 log.debug("Removing %s reviewers", ids_to_remove)
728 changed = False
728 changed = False
729 for uid in ids_to_add:
729 for uid in ids_to_add:
730 changed = True
730 changed = True
731 _usr = self._get_user(uid)
731 _usr = self._get_user(uid)
732 reviewer = PullRequestReviewers(_usr, pull_request)
732 reviewer = PullRequestReviewers(_usr, pull_request)
733 Session().add(reviewer)
733 Session().add(reviewer)
734
734
735 self.notify_reviewers(pull_request, ids_to_add)
735 self.notify_reviewers(pull_request, ids_to_add)
736
736
737 for uid in ids_to_remove:
737 for uid in ids_to_remove:
738 changed = True
738 changed = True
739 reviewer = PullRequestReviewers.query()\
739 reviewer = PullRequestReviewers.query()\
740 .filter(PullRequestReviewers.user_id == uid,
740 .filter(PullRequestReviewers.user_id == uid,
741 PullRequestReviewers.pull_request == pull_request)\
741 PullRequestReviewers.pull_request == pull_request)\
742 .scalar()
742 .scalar()
743 if reviewer:
743 if reviewer:
744 Session().delete(reviewer)
744 Session().delete(reviewer)
745 if changed:
745 if changed:
746 pull_request.updated_on = datetime.datetime.now()
746 pull_request.updated_on = datetime.datetime.now()
747 Session().add(pull_request)
747 Session().add(pull_request)
748
748
749 return ids_to_add, ids_to_remove
749 return ids_to_add, ids_to_remove
750
750
751 def get_url(self, pull_request):
751 def get_url(self, pull_request):
752 return h.url('pullrequest_show', repo_name=self.target_repo.repo_name,
752 return h.url('pullrequest_show',
753 pull_request_id=self.pull_request_id,
753 repo_name=pull_request.target_repo.repo_name,
754 qualified=True)
754 pull_request_id=pull_request.pull_request_id,
755 qualified=True)
755
756
756 def notify_reviewers(self, pull_request, reviewers_ids):
757 def notify_reviewers(self, pull_request, reviewers_ids):
757 # notification to reviewers
758 # notification to reviewers
758 if not reviewers_ids:
759 if not reviewers_ids:
759 return
760 return
760
761
761 pull_request_obj = pull_request
762 pull_request_obj = pull_request
762 # get the current participants of this pull request
763 # get the current participants of this pull request
763 recipients = reviewers_ids
764 recipients = reviewers_ids
764 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
765 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
765
766
766 pr_source_repo = pull_request_obj.source_repo
767 pr_source_repo = pull_request_obj.source_repo
767 pr_target_repo = pull_request_obj.target_repo
768 pr_target_repo = pull_request_obj.target_repo
768
769
769 pr_url = h.url(
770 pr_url = h.url(
770 'pullrequest_show',
771 'pullrequest_show',
771 repo_name=pr_target_repo.repo_name,
772 repo_name=pr_target_repo.repo_name,
772 pull_request_id=pull_request_obj.pull_request_id,
773 pull_request_id=pull_request_obj.pull_request_id,
773 qualified=True,)
774 qualified=True,)
774
775
775 # set some variables for email notification
776 # set some variables for email notification
776 pr_target_repo_url = h.url(
777 pr_target_repo_url = h.url(
777 'summary_home',
778 'summary_home',
778 repo_name=pr_target_repo.repo_name,
779 repo_name=pr_target_repo.repo_name,
779 qualified=True)
780 qualified=True)
780
781
781 pr_source_repo_url = h.url(
782 pr_source_repo_url = h.url(
782 'summary_home',
783 'summary_home',
783 repo_name=pr_source_repo.repo_name,
784 repo_name=pr_source_repo.repo_name,
784 qualified=True)
785 qualified=True)
785
786
786 # pull request specifics
787 # pull request specifics
787 pull_request_commits = [
788 pull_request_commits = [
788 (x.raw_id, x.message)
789 (x.raw_id, x.message)
789 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
790 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
790
791
791 kwargs = {
792 kwargs = {
792 'user': pull_request.author,
793 'user': pull_request.author,
793 'pull_request': pull_request_obj,
794 'pull_request': pull_request_obj,
794 'pull_request_commits': pull_request_commits,
795 'pull_request_commits': pull_request_commits,
795
796
796 'pull_request_target_repo': pr_target_repo,
797 'pull_request_target_repo': pr_target_repo,
797 'pull_request_target_repo_url': pr_target_repo_url,
798 'pull_request_target_repo_url': pr_target_repo_url,
798
799
799 'pull_request_source_repo': pr_source_repo,
800 'pull_request_source_repo': pr_source_repo,
800 'pull_request_source_repo_url': pr_source_repo_url,
801 'pull_request_source_repo_url': pr_source_repo_url,
801
802
802 'pull_request_url': pr_url,
803 'pull_request_url': pr_url,
803 }
804 }
804
805
805 # pre-generate the subject for notification itself
806 # pre-generate the subject for notification itself
806 (subject,
807 (subject,
807 _h, _e, # we don't care about those
808 _h, _e, # we don't care about those
808 body_plaintext) = EmailNotificationModel().render_email(
809 body_plaintext) = EmailNotificationModel().render_email(
809 notification_type, **kwargs)
810 notification_type, **kwargs)
810
811
811 # create notification objects, and emails
812 # create notification objects, and emails
812 NotificationModel().create(
813 NotificationModel().create(
813 created_by=pull_request.author,
814 created_by=pull_request.author,
814 notification_subject=subject,
815 notification_subject=subject,
815 notification_body=body_plaintext,
816 notification_body=body_plaintext,
816 notification_type=notification_type,
817 notification_type=notification_type,
817 recipients=recipients,
818 recipients=recipients,
818 email_kwargs=kwargs,
819 email_kwargs=kwargs,
819 )
820 )
820
821
821 def delete(self, pull_request):
822 def delete(self, pull_request):
822 pull_request = self.__get_pull_request(pull_request)
823 pull_request = self.__get_pull_request(pull_request)
823 self._cleanup_merge_workspace(pull_request)
824 self._cleanup_merge_workspace(pull_request)
824 Session().delete(pull_request)
825 Session().delete(pull_request)
825
826
826 def close_pull_request(self, pull_request, user):
827 def close_pull_request(self, pull_request, user):
827 pull_request = self.__get_pull_request(pull_request)
828 pull_request = self.__get_pull_request(pull_request)
828 self._cleanup_merge_workspace(pull_request)
829 self._cleanup_merge_workspace(pull_request)
829 pull_request.status = PullRequest.STATUS_CLOSED
830 pull_request.status = PullRequest.STATUS_CLOSED
830 pull_request.updated_on = datetime.datetime.now()
831 pull_request.updated_on = datetime.datetime.now()
831 Session().add(pull_request)
832 Session().add(pull_request)
832 self._trigger_pull_request_hook(
833 self._trigger_pull_request_hook(
833 pull_request, pull_request.author, 'close')
834 pull_request, pull_request.author, 'close')
834 self._log_action('user_closed_pull_request', user, pull_request)
835 self._log_action('user_closed_pull_request', user, pull_request)
835
836
836 def close_pull_request_with_comment(self, pull_request, user, repo,
837 def close_pull_request_with_comment(self, pull_request, user, repo,
837 message=None):
838 message=None):
838 status = ChangesetStatus.STATUS_REJECTED
839 status = ChangesetStatus.STATUS_REJECTED
839
840
840 if not message:
841 if not message:
841 message = (
842 message = (
842 _('Status change %(transition_icon)s %(status)s') % {
843 _('Status change %(transition_icon)s %(status)s') % {
843 'transition_icon': '>',
844 'transition_icon': '>',
844 'status': ChangesetStatus.get_status_lbl(status)})
845 'status': ChangesetStatus.get_status_lbl(status)})
845
846
846 internal_message = _('Closing with') + ' ' + message
847 internal_message = _('Closing with') + ' ' + message
847
848
848 comm = ChangesetCommentsModel().create(
849 comm = ChangesetCommentsModel().create(
849 text=internal_message,
850 text=internal_message,
850 repo=repo.repo_id,
851 repo=repo.repo_id,
851 user=user.user_id,
852 user=user.user_id,
852 pull_request=pull_request.pull_request_id,
853 pull_request=pull_request.pull_request_id,
853 f_path=None,
854 f_path=None,
854 line_no=None,
855 line_no=None,
855 status_change=ChangesetStatus.get_status_lbl(status),
856 status_change=ChangesetStatus.get_status_lbl(status),
856 closing_pr=True
857 closing_pr=True
857 )
858 )
858
859
859 ChangesetStatusModel().set_status(
860 ChangesetStatusModel().set_status(
860 repo.repo_id,
861 repo.repo_id,
861 status,
862 status,
862 user.user_id,
863 user.user_id,
863 comm,
864 comm,
864 pull_request=pull_request.pull_request_id
865 pull_request=pull_request.pull_request_id
865 )
866 )
866 Session().flush()
867 Session().flush()
867
868
868 PullRequestModel().close_pull_request(
869 PullRequestModel().close_pull_request(
869 pull_request.pull_request_id, user)
870 pull_request.pull_request_id, user)
870
871
871 def merge_status(self, pull_request):
872 def merge_status(self, pull_request):
872 if not self._is_merge_enabled(pull_request):
873 if not self._is_merge_enabled(pull_request):
873 return False, _('Server-side pull request merging is disabled.')
874 return False, _('Server-side pull request merging is disabled.')
874 if pull_request.is_closed():
875 if pull_request.is_closed():
875 return False, _('This pull request is closed.')
876 return False, _('This pull request is closed.')
876 merge_possible, msg = self._check_repo_requirements(
877 merge_possible, msg = self._check_repo_requirements(
877 target=pull_request.target_repo, source=pull_request.source_repo)
878 target=pull_request.target_repo, source=pull_request.source_repo)
878 if not merge_possible:
879 if not merge_possible:
879 return merge_possible, msg
880 return merge_possible, msg
880
881
881 try:
882 try:
882 resp = self._try_merge(pull_request)
883 resp = self._try_merge(pull_request)
883 status = resp.possible, self.merge_status_message(
884 status = resp.possible, self.merge_status_message(
884 resp.failure_reason)
885 resp.failure_reason)
885 except NotImplementedError:
886 except NotImplementedError:
886 status = False, _('Pull request merging is not supported.')
887 status = False, _('Pull request merging is not supported.')
887
888
888 return status
889 return status
889
890
890 def _check_repo_requirements(self, target, source):
891 def _check_repo_requirements(self, target, source):
891 """
892 """
892 Check if `target` and `source` have compatible requirements.
893 Check if `target` and `source` have compatible requirements.
893
894
894 Currently this is just checking for largefiles.
895 Currently this is just checking for largefiles.
895 """
896 """
896 target_has_largefiles = self._has_largefiles(target)
897 target_has_largefiles = self._has_largefiles(target)
897 source_has_largefiles = self._has_largefiles(source)
898 source_has_largefiles = self._has_largefiles(source)
898 merge_possible = True
899 merge_possible = True
899 message = u''
900 message = u''
900
901
901 if target_has_largefiles != source_has_largefiles:
902 if target_has_largefiles != source_has_largefiles:
902 merge_possible = False
903 merge_possible = False
903 if source_has_largefiles:
904 if source_has_largefiles:
904 message = _(
905 message = _(
905 'Target repository large files support is disabled.')
906 'Target repository large files support is disabled.')
906 else:
907 else:
907 message = _(
908 message = _(
908 'Source repository large files support is disabled.')
909 'Source repository large files support is disabled.')
909
910
910 return merge_possible, message
911 return merge_possible, message
911
912
912 def _has_largefiles(self, repo):
913 def _has_largefiles(self, repo):
913 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
914 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
914 'extensions', 'largefiles')
915 'extensions', 'largefiles')
915 return largefiles_ui and largefiles_ui[0].active
916 return largefiles_ui and largefiles_ui[0].active
916
917
917 def _try_merge(self, pull_request):
918 def _try_merge(self, pull_request):
918 """
919 """
919 Try to merge the pull request and return the merge status.
920 Try to merge the pull request and return the merge status.
920 """
921 """
921 log.debug(
922 log.debug(
922 "Trying out if the pull request %s can be merged.",
923 "Trying out if the pull request %s can be merged.",
923 pull_request.pull_request_id)
924 pull_request.pull_request_id)
924 target_vcs = pull_request.target_repo.scm_instance()
925 target_vcs = pull_request.target_repo.scm_instance()
925 target_ref = self._refresh_reference(
926 target_ref = self._refresh_reference(
926 pull_request.target_ref_parts, target_vcs)
927 pull_request.target_ref_parts, target_vcs)
927
928
928 target_locked = pull_request.target_repo.locked
929 target_locked = pull_request.target_repo.locked
929 if target_locked and target_locked[0]:
930 if target_locked and target_locked[0]:
930 log.debug("The target repository is locked.")
931 log.debug("The target repository is locked.")
931 merge_state = MergeResponse(
932 merge_state = MergeResponse(
932 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
933 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
933 elif self._needs_merge_state_refresh(pull_request, target_ref):
934 elif self._needs_merge_state_refresh(pull_request, target_ref):
934 log.debug("Refreshing the merge status of the repository.")
935 log.debug("Refreshing the merge status of the repository.")
935 merge_state = self._refresh_merge_state(
936 merge_state = self._refresh_merge_state(
936 pull_request, target_vcs, target_ref)
937 pull_request, target_vcs, target_ref)
937 else:
938 else:
938 possible = pull_request.\
939 possible = pull_request.\
939 _last_merge_status == MergeFailureReason.NONE
940 _last_merge_status == MergeFailureReason.NONE
940 merge_state = MergeResponse(
941 merge_state = MergeResponse(
941 possible, False, None, pull_request._last_merge_status)
942 possible, False, None, pull_request._last_merge_status)
942 log.debug("Merge response: %s", merge_state)
943 log.debug("Merge response: %s", merge_state)
943 return merge_state
944 return merge_state
944
945
945 def _refresh_reference(self, reference, vcs_repository):
946 def _refresh_reference(self, reference, vcs_repository):
946 if reference.type in ('branch', 'book'):
947 if reference.type in ('branch', 'book'):
947 name_or_id = reference.name
948 name_or_id = reference.name
948 else:
949 else:
949 name_or_id = reference.commit_id
950 name_or_id = reference.commit_id
950 refreshed_commit = vcs_repository.get_commit(name_or_id)
951 refreshed_commit = vcs_repository.get_commit(name_or_id)
951 refreshed_reference = Reference(
952 refreshed_reference = Reference(
952 reference.type, reference.name, refreshed_commit.raw_id)
953 reference.type, reference.name, refreshed_commit.raw_id)
953 return refreshed_reference
954 return refreshed_reference
954
955
955 def _needs_merge_state_refresh(self, pull_request, target_reference):
956 def _needs_merge_state_refresh(self, pull_request, target_reference):
956 return not(
957 return not(
957 pull_request.revisions and
958 pull_request.revisions and
958 pull_request.revisions[0] == pull_request._last_merge_source_rev and
959 pull_request.revisions[0] == pull_request._last_merge_source_rev and
959 target_reference.commit_id == pull_request._last_merge_target_rev)
960 target_reference.commit_id == pull_request._last_merge_target_rev)
960
961
961 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
962 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
962 workspace_id = self._workspace_id(pull_request)
963 workspace_id = self._workspace_id(pull_request)
963 source_vcs = pull_request.source_repo.scm_instance()
964 source_vcs = pull_request.source_repo.scm_instance()
964 use_rebase = self._use_rebase_for_merging(pull_request)
965 use_rebase = self._use_rebase_for_merging(pull_request)
965 merge_state = target_vcs.merge(
966 merge_state = target_vcs.merge(
966 target_reference, source_vcs, pull_request.source_ref_parts,
967 target_reference, source_vcs, pull_request.source_ref_parts,
967 workspace_id, dry_run=True, use_rebase=use_rebase)
968 workspace_id, dry_run=True, use_rebase=use_rebase)
968
969
969 # Do not store the response if there was an unknown error.
970 # Do not store the response if there was an unknown error.
970 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
971 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
971 pull_request._last_merge_source_rev = pull_request.\
972 pull_request._last_merge_source_rev = pull_request.\
972 source_ref_parts.commit_id
973 source_ref_parts.commit_id
973 pull_request._last_merge_target_rev = target_reference.commit_id
974 pull_request._last_merge_target_rev = target_reference.commit_id
974 pull_request._last_merge_status = (
975 pull_request._last_merge_status = (
975 merge_state.failure_reason)
976 merge_state.failure_reason)
976 Session().add(pull_request)
977 Session().add(pull_request)
977 Session().flush()
978 Session().flush()
978
979
979 return merge_state
980 return merge_state
980
981
981 def _workspace_id(self, pull_request):
982 def _workspace_id(self, pull_request):
982 workspace_id = 'pr-%s' % pull_request.pull_request_id
983 workspace_id = 'pr-%s' % pull_request.pull_request_id
983 return workspace_id
984 return workspace_id
984
985
985 def merge_status_message(self, status_code):
986 def merge_status_message(self, status_code):
986 """
987 """
987 Return a human friendly error message for the given merge status code.
988 Return a human friendly error message for the given merge status code.
988 """
989 """
989 return self.MERGE_STATUS_MESSAGES[status_code]
990 return self.MERGE_STATUS_MESSAGES[status_code]
990
991
991 def generate_repo_data(self, repo, commit_id=None, branch=None,
992 def generate_repo_data(self, repo, commit_id=None, branch=None,
992 bookmark=None):
993 bookmark=None):
993 all_refs, selected_ref = \
994 all_refs, selected_ref = \
994 self._get_repo_pullrequest_sources(
995 self._get_repo_pullrequest_sources(
995 repo.scm_instance(), commit_id=commit_id,
996 repo.scm_instance(), commit_id=commit_id,
996 branch=branch, bookmark=bookmark)
997 branch=branch, bookmark=bookmark)
997
998
998 refs_select2 = []
999 refs_select2 = []
999 for element in all_refs:
1000 for element in all_refs:
1000 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1001 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1001 refs_select2.append({'text': element[1], 'children': children})
1002 refs_select2.append({'text': element[1], 'children': children})
1002
1003
1003 return {
1004 return {
1004 'user': {
1005 'user': {
1005 'user_id': repo.user.user_id,
1006 'user_id': repo.user.user_id,
1006 'username': repo.user.username,
1007 'username': repo.user.username,
1007 'firstname': repo.user.firstname,
1008 'firstname': repo.user.firstname,
1008 'lastname': repo.user.lastname,
1009 'lastname': repo.user.lastname,
1009 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1010 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1010 },
1011 },
1011 'description': h.chop_at_smart(repo.description, '\n'),
1012 'description': h.chop_at_smart(repo.description, '\n'),
1012 'refs': {
1013 'refs': {
1013 'all_refs': all_refs,
1014 'all_refs': all_refs,
1014 'selected_ref': selected_ref,
1015 'selected_ref': selected_ref,
1015 'select2_refs': refs_select2
1016 'select2_refs': refs_select2
1016 }
1017 }
1017 }
1018 }
1018
1019
1019 def generate_pullrequest_title(self, source, source_ref, target):
1020 def generate_pullrequest_title(self, source, source_ref, target):
1020 return '{source}#{at_ref} to {target}'.format(
1021 return '{source}#{at_ref} to {target}'.format(
1021 source=source,
1022 source=source,
1022 at_ref=source_ref,
1023 at_ref=source_ref,
1023 target=target,
1024 target=target,
1024 )
1025 )
1025
1026
1026 def _cleanup_merge_workspace(self, pull_request):
1027 def _cleanup_merge_workspace(self, pull_request):
1027 # Merging related cleanup
1028 # Merging related cleanup
1028 target_scm = pull_request.target_repo.scm_instance()
1029 target_scm = pull_request.target_repo.scm_instance()
1029 workspace_id = 'pr-%s' % pull_request.pull_request_id
1030 workspace_id = 'pr-%s' % pull_request.pull_request_id
1030
1031
1031 try:
1032 try:
1032 target_scm.cleanup_merge_workspace(workspace_id)
1033 target_scm.cleanup_merge_workspace(workspace_id)
1033 except NotImplementedError:
1034 except NotImplementedError:
1034 pass
1035 pass
1035
1036
1036 def _get_repo_pullrequest_sources(
1037 def _get_repo_pullrequest_sources(
1037 self, repo, commit_id=None, branch=None, bookmark=None):
1038 self, repo, commit_id=None, branch=None, bookmark=None):
1038 """
1039 """
1039 Return a structure with repo's interesting commits, suitable for
1040 Return a structure with repo's interesting commits, suitable for
1040 the selectors in pullrequest controller
1041 the selectors in pullrequest controller
1041
1042
1042 :param commit_id: a commit that must be in the list somehow
1043 :param commit_id: a commit that must be in the list somehow
1043 and selected by default
1044 and selected by default
1044 :param branch: a branch that must be in the list and selected
1045 :param branch: a branch that must be in the list and selected
1045 by default - even if closed
1046 by default - even if closed
1046 :param bookmark: a bookmark that must be in the list and selected
1047 :param bookmark: a bookmark that must be in the list and selected
1047 """
1048 """
1048
1049
1049 commit_id = safe_str(commit_id) if commit_id else None
1050 commit_id = safe_str(commit_id) if commit_id else None
1050 branch = safe_str(branch) if branch else None
1051 branch = safe_str(branch) if branch else None
1051 bookmark = safe_str(bookmark) if bookmark else None
1052 bookmark = safe_str(bookmark) if bookmark else None
1052
1053
1053 selected = None
1054 selected = None
1054
1055
1055 # order matters: first source that has commit_id in it will be selected
1056 # order matters: first source that has commit_id in it will be selected
1056 sources = []
1057 sources = []
1057 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1058 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1058 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1059 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1059
1060
1060 if commit_id:
1061 if commit_id:
1061 ref_commit = (h.short_id(commit_id), commit_id)
1062 ref_commit = (h.short_id(commit_id), commit_id)
1062 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1063 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1063
1064
1064 sources.append(
1065 sources.append(
1065 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1066 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1066 )
1067 )
1067
1068
1068 groups = []
1069 groups = []
1069 for group_key, ref_list, group_name, match in sources:
1070 for group_key, ref_list, group_name, match in sources:
1070 group_refs = []
1071 group_refs = []
1071 for ref_name, ref_id in ref_list:
1072 for ref_name, ref_id in ref_list:
1072 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1073 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1073 group_refs.append((ref_key, ref_name))
1074 group_refs.append((ref_key, ref_name))
1074
1075
1075 if not selected:
1076 if not selected:
1076 if set([commit_id, match]) & set([ref_id, ref_name]):
1077 if set([commit_id, match]) & set([ref_id, ref_name]):
1077 selected = ref_key
1078 selected = ref_key
1078
1079
1079 if group_refs:
1080 if group_refs:
1080 groups.append((group_refs, group_name))
1081 groups.append((group_refs, group_name))
1081
1082
1082 if not selected:
1083 if not selected:
1083 ref = commit_id or branch or bookmark
1084 ref = commit_id or branch or bookmark
1084 if ref:
1085 if ref:
1085 raise CommitDoesNotExistError(
1086 raise CommitDoesNotExistError(
1086 'No commit refs could be found matching: %s' % ref)
1087 'No commit refs could be found matching: %s' % ref)
1087 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1088 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1088 selected = 'branch:%s:%s' % (
1089 selected = 'branch:%s:%s' % (
1089 repo.DEFAULT_BRANCH_NAME,
1090 repo.DEFAULT_BRANCH_NAME,
1090 repo.branches[repo.DEFAULT_BRANCH_NAME]
1091 repo.branches[repo.DEFAULT_BRANCH_NAME]
1091 )
1092 )
1092 elif repo.commit_ids:
1093 elif repo.commit_ids:
1093 rev = repo.commit_ids[0]
1094 rev = repo.commit_ids[0]
1094 selected = 'rev:%s:%s' % (rev, rev)
1095 selected = 'rev:%s:%s' % (rev, rev)
1095 else:
1096 else:
1096 raise EmptyRepositoryError()
1097 raise EmptyRepositoryError()
1097 return groups, selected
1098 return groups, selected
1098
1099
1099 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1100 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1100 pull_request = self.__get_pull_request(pull_request)
1101 pull_request = self.__get_pull_request(pull_request)
1101 return self._get_diff_from_pr_or_version(pull_request, context=context)
1102 return self._get_diff_from_pr_or_version(pull_request, context=context)
1102
1103
1103 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1104 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1104 source_repo = pr_or_version.source_repo
1105 source_repo = pr_or_version.source_repo
1105
1106
1106 # we swap org/other ref since we run a simple diff on one repo
1107 # we swap org/other ref since we run a simple diff on one repo
1107 target_ref_id = pr_or_version.target_ref_parts.commit_id
1108 target_ref_id = pr_or_version.target_ref_parts.commit_id
1108 source_ref_id = pr_or_version.source_ref_parts.commit_id
1109 source_ref_id = pr_or_version.source_ref_parts.commit_id
1109 target_commit = source_repo.get_commit(
1110 target_commit = source_repo.get_commit(
1110 commit_id=safe_str(target_ref_id))
1111 commit_id=safe_str(target_ref_id))
1111 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1112 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1112 vcs_repo = source_repo.scm_instance()
1113 vcs_repo = source_repo.scm_instance()
1113
1114
1114 # TODO: johbo: In the context of an update, we cannot reach
1115 # TODO: johbo: In the context of an update, we cannot reach
1115 # the old commit anymore with our normal mechanisms. It needs
1116 # the old commit anymore with our normal mechanisms. It needs
1116 # some sort of special support in the vcs layer to avoid this
1117 # some sort of special support in the vcs layer to avoid this
1117 # workaround.
1118 # workaround.
1118 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1119 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1119 vcs_repo.alias == 'git'):
1120 vcs_repo.alias == 'git'):
1120 source_commit.raw_id = safe_str(source_ref_id)
1121 source_commit.raw_id = safe_str(source_ref_id)
1121
1122
1122 log.debug('calculating diff between '
1123 log.debug('calculating diff between '
1123 'source_ref:%s and target_ref:%s for repo `%s`',
1124 'source_ref:%s and target_ref:%s for repo `%s`',
1124 target_ref_id, source_ref_id,
1125 target_ref_id, source_ref_id,
1125 safe_unicode(vcs_repo.path))
1126 safe_unicode(vcs_repo.path))
1126
1127
1127 vcs_diff = vcs_repo.get_diff(
1128 vcs_diff = vcs_repo.get_diff(
1128 commit1=target_commit, commit2=source_commit, context=context)
1129 commit1=target_commit, commit2=source_commit, context=context)
1129 return vcs_diff
1130 return vcs_diff
1130
1131
1131 def _is_merge_enabled(self, pull_request):
1132 def _is_merge_enabled(self, pull_request):
1132 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1133 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1133 settings = settings_model.get_general_settings()
1134 settings = settings_model.get_general_settings()
1134 return settings.get('rhodecode_pr_merge_enabled', False)
1135 return settings.get('rhodecode_pr_merge_enabled', False)
1135
1136
1136 def _use_rebase_for_merging(self, pull_request):
1137 def _use_rebase_for_merging(self, pull_request):
1137 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1138 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1138 settings = settings_model.get_general_settings()
1139 settings = settings_model.get_general_settings()
1139 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1140 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1140
1141
1141 def _log_action(self, action, user, pull_request):
1142 def _log_action(self, action, user, pull_request):
1142 action_logger(
1143 action_logger(
1143 user,
1144 user,
1144 '{action}:{pr_id}'.format(
1145 '{action}:{pr_id}'.format(
1145 action=action, pr_id=pull_request.pull_request_id),
1146 action=action, pr_id=pull_request.pull_request_id),
1146 pull_request.target_repo)
1147 pull_request.target_repo)
1147
1148
1148
1149
1149 ChangeTuple = namedtuple('ChangeTuple',
1150 ChangeTuple = namedtuple('ChangeTuple',
1150 ['added', 'common', 'removed'])
1151 ['added', 'common', 'removed'])
1151
1152
1152 FileChangeTuple = namedtuple('FileChangeTuple',
1153 FileChangeTuple = namedtuple('FileChangeTuple',
1153 ['added', 'modified', 'removed'])
1154 ['added', 'modified', 'removed'])
@@ -1,78 +1,78 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.tests.events.conftest import EventCatcher
23 from rhodecode.tests.events.conftest import EventCatcher
24
24
25 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.pull_request import PullRequestModel
26 from rhodecode.events import (
26 from rhodecode.events import (
27 PullRequestCreateEvent,
27 PullRequestCreateEvent,
28 PullRequestUpdateEvent,
28 PullRequestUpdateEvent,
29 PullRequestReviewEvent,
29 PullRequestReviewEvent,
30 PullRequestMergeEvent,
30 PullRequestMergeEvent,
31 PullRequestCloseEvent,
31 PullRequestCloseEvent,
32 )
32 )
33
33
34 # TODO: dan: make the serialization tests complete json comparisons
34 # TODO: dan: make the serialization tests complete json comparisons
35 @pytest.mark.backends("git", "hg")
35 @pytest.mark.backends("git", "hg")
36 @pytest.mark.parametrize('EventClass', [
36 @pytest.mark.parametrize('EventClass', [
37 PullRequestCreateEvent,
37 PullRequestCreateEvent,
38 PullRequestUpdateEvent,
38 PullRequestUpdateEvent,
39 PullRequestReviewEvent,
39 PullRequestReviewEvent,
40 PullRequestMergeEvent,
40 PullRequestMergeEvent,
41 PullRequestCloseEvent,
41 PullRequestCloseEvent,
42 ])
42 ])
43 def test_pullrequest_events_serialized(pr_util, EventClass):
43 def test_pullrequest_events_serialized(pr_util, EventClass):
44 pr = pr_util.create_pull_request()
44 pr = pr_util.create_pull_request()
45 event = EventClass(pr)
45 event = EventClass(pr)
46 data = event.as_dict()
46 data = event.as_dict()
47 assert data['name'] == EventClass.name
47 assert data['name'] == EventClass.name
48 assert data['repo']['repo_name'] == pr.target_repo.repo_name
48 assert data['repo']['repo_name'] == pr.target_repo.repo_name
49 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
49 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
50
50 assert data['pullrequest']['url']
51
51
52 @pytest.mark.backends("git", "hg")
52 @pytest.mark.backends("git", "hg")
53 def test_create_pull_request_events(pr_util):
53 def test_create_pull_request_events(pr_util):
54 with EventCatcher() as event_catcher:
54 with EventCatcher() as event_catcher:
55 pr_util.create_pull_request()
55 pr_util.create_pull_request()
56
56
57 assert PullRequestCreateEvent in event_catcher.events_types
57 assert PullRequestCreateEvent in event_catcher.events_types
58
58
59
59
60 @pytest.mark.backends("git", "hg")
60 @pytest.mark.backends("git", "hg")
61 def test_close_pull_request_events(pr_util, user_admin):
61 def test_close_pull_request_events(pr_util, user_admin):
62 pr = pr_util.create_pull_request()
62 pr = pr_util.create_pull_request()
63
63
64 with EventCatcher() as event_catcher:
64 with EventCatcher() as event_catcher:
65 PullRequestModel().close_pull_request(pr, user_admin)
65 PullRequestModel().close_pull_request(pr, user_admin)
66
66
67 assert PullRequestCloseEvent in event_catcher.events_types
67 assert PullRequestCloseEvent in event_catcher.events_types
68
68
69
69
70 @pytest.mark.backends("git", "hg")
70 @pytest.mark.backends("git", "hg")
71 def test_close_pull_request_with_comment_events(pr_util, user_admin):
71 def test_close_pull_request_with_comment_events(pr_util, user_admin):
72 pr = pr_util.create_pull_request()
72 pr = pr_util.create_pull_request()
73
73
74 with EventCatcher() as event_catcher:
74 with EventCatcher() as event_catcher:
75 PullRequestModel().close_pull_request_with_comment(
75 PullRequestModel().close_pull_request_with_comment(
76 pr, user_admin, pr.target_repo)
76 pr, user_admin, pr.target_repo)
77
77
78 assert PullRequestCloseEvent in event_catcher.events_types
78 assert PullRequestCloseEvent in event_catcher.events_types
@@ -1,113 +1,116 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.tests.events.conftest import EventCatcher
23 from rhodecode.tests.events.conftest import EventCatcher
24
24
25 from rhodecode.lib import hooks_base, utils2
25 from rhodecode.lib import hooks_base, utils2
26 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.repo import RepoModel
27 from rhodecode.events.repo import (
27 from rhodecode.events.repo import (
28 RepoPrePullEvent, RepoPullEvent,
28 RepoPrePullEvent, RepoPullEvent,
29 RepoPrePushEvent, RepoPushEvent,
29 RepoPrePushEvent, RepoPushEvent,
30 RepoPreCreateEvent, RepoCreatedEvent,
30 RepoPreCreateEvent, RepoCreatedEvent,
31 RepoPreDeleteEvent, RepoDeletedEvent,
31 RepoPreDeleteEvent, RepoDeletedEvent,
32 )
32 )
33
33
34
34
35 @pytest.fixture
35 @pytest.fixture
36 def scm_extras(user_regular, repo_stub):
36 def scm_extras(user_regular, repo_stub):
37 extras = utils2.AttributeDict({
37 extras = utils2.AttributeDict({
38 'ip': '127.0.0.1',
38 'ip': '127.0.0.1',
39 'username': user_regular.username,
39 'username': user_regular.username,
40 'action': '',
40 'action': '',
41 'repository': repo_stub.repo_name,
41 'repository': repo_stub.repo_name,
42 'scm': repo_stub.scm_instance().alias,
42 'scm': repo_stub.scm_instance().alias,
43 'config': '',
43 'config': '',
44 'server_url': 'http://example.com',
44 'server_url': 'http://example.com',
45 'make_lock': None,
45 'make_lock': None,
46 'locked_by': [None],
46 'locked_by': [None],
47 'commit_ids': ['a' * 40] * 3,
47 'commit_ids': ['a' * 40] * 3,
48 })
48 })
49 return extras
49 return extras
50
50
51
51
52 # TODO: dan: make the serialization tests complete json comparisons
52 # TODO: dan: make the serialization tests complete json comparisons
53 @pytest.mark.parametrize('EventClass', [
53 @pytest.mark.parametrize('EventClass', [
54 RepoPreCreateEvent, RepoCreatedEvent,
54 RepoPreCreateEvent, RepoCreatedEvent,
55 RepoPreDeleteEvent, RepoDeletedEvent,
55 RepoPreDeleteEvent, RepoDeletedEvent,
56 ])
56 ])
57 def test_repo_events_serialized(repo_stub, EventClass):
57 def test_repo_events_serialized(repo_stub, EventClass):
58 event = EventClass(repo_stub)
58 event = EventClass(repo_stub)
59 data = event.as_dict()
59 data = event.as_dict()
60 assert data['name'] == EventClass.name
60 assert data['name'] == EventClass.name
61 assert data['repo']['repo_name'] == repo_stub.repo_name
61 assert data['repo']['repo_name'] == repo_stub.repo_name
62 assert data['repo']['url']
62
63
63
64
64 @pytest.mark.parametrize('EventClass', [
65 @pytest.mark.parametrize('EventClass', [
65 RepoPrePullEvent, RepoPullEvent, RepoPrePushEvent
66 RepoPrePullEvent, RepoPullEvent, RepoPrePushEvent
66 ])
67 ])
67 def test_vcs_repo_events_serialize(repo_stub, scm_extras, EventClass):
68 def test_vcs_repo_events_serialize(repo_stub, scm_extras, EventClass):
68 event = EventClass(repo_name=repo_stub.repo_name, extras=scm_extras)
69 event = EventClass(repo_name=repo_stub.repo_name, extras=scm_extras)
69 data = event.as_dict()
70 data = event.as_dict()
70 assert data['name'] == EventClass.name
71 assert data['name'] == EventClass.name
71 assert data['repo']['repo_name'] == repo_stub.repo_name
72 assert data['repo']['repo_name'] == repo_stub.repo_name
73 assert data['repo']['url']
72
74
73
75
74
76
75 @pytest.mark.parametrize('EventClass', [RepoPushEvent])
77 @pytest.mark.parametrize('EventClass', [RepoPushEvent])
76 def test_vcs_repo_events_serialize(repo_stub, scm_extras, EventClass):
78 def test_vcs_repo_push_event_serialize(repo_stub, scm_extras, EventClass):
77 event = EventClass(repo_name=repo_stub.repo_name,
79 event = EventClass(repo_name=repo_stub.repo_name,
78 pushed_commit_ids=scm_extras['commit_ids'],
80 pushed_commit_ids=scm_extras['commit_ids'],
79 extras=scm_extras)
81 extras=scm_extras)
80 data = event.as_dict()
82 data = event.as_dict()
81 assert data['name'] == EventClass.name
83 assert data['name'] == EventClass.name
82 assert data['repo']['repo_name'] == repo_stub.repo_name
84 assert data['repo']['repo_name'] == repo_stub.repo_name
85 assert data['repo']['url']
83
86
84
87
85 def test_create_delete_repo_fires_events(backend):
88 def test_create_delete_repo_fires_events(backend):
86 with EventCatcher() as event_catcher:
89 with EventCatcher() as event_catcher:
87 repo = backend.create_repo()
90 repo = backend.create_repo()
88 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreatedEvent]
91 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreatedEvent]
89
92
90 with EventCatcher() as event_catcher:
93 with EventCatcher() as event_catcher:
91 RepoModel().delete(repo)
94 RepoModel().delete(repo)
92 assert event_catcher.events_types == [RepoPreDeleteEvent, RepoDeletedEvent]
95 assert event_catcher.events_types == [RepoPreDeleteEvent, RepoDeletedEvent]
93
96
94
97
95 def test_pull_fires_events(scm_extras):
98 def test_pull_fires_events(scm_extras):
96 with EventCatcher() as event_catcher:
99 with EventCatcher() as event_catcher:
97 hooks_base.pre_push(scm_extras)
100 hooks_base.pre_push(scm_extras)
98 assert event_catcher.events_types == [RepoPrePushEvent]
101 assert event_catcher.events_types == [RepoPrePushEvent]
99
102
100 with EventCatcher() as event_catcher:
103 with EventCatcher() as event_catcher:
101 hooks_base.post_push(scm_extras)
104 hooks_base.post_push(scm_extras)
102 assert event_catcher.events_types == [RepoPushEvent]
105 assert event_catcher.events_types == [RepoPushEvent]
103
106
104
107
105 def test_push_fires_events(scm_extras):
108 def test_push_fires_events(scm_extras):
106 with EventCatcher() as event_catcher:
109 with EventCatcher() as event_catcher:
107 hooks_base.pre_pull(scm_extras)
110 hooks_base.pre_pull(scm_extras)
108 assert event_catcher.events_types == [RepoPrePullEvent]
111 assert event_catcher.events_types == [RepoPrePullEvent]
109
112
110 with EventCatcher() as event_catcher:
113 with EventCatcher() as event_catcher:
111 hooks_base.post_pull(scm_extras)
114 hooks_base.post_pull(scm_extras)
112 assert event_catcher.events_types == [RepoPullEvent]
115 assert event_catcher.events_types == [RepoPullEvent]
113
116
General Comments 0
You need to be logged in to leave comments. Login now