##// END OF EJS Templates
pull-request-api: updated logic of closing a PR via API call....
marcink -
r1792:a62f3dac default
parent child Browse files
Show More
@@ -1,112 +1,113 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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.model.db import UserLog
23 from rhodecode.model.db import UserLog
24 from rhodecode.model.pull_request import PullRequestModel
24 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error, assert_ok)
27 build_data, api_call, assert_error, assert_ok)
28
28
29
29
30 @pytest.mark.usefixtures("testuser_api", "app")
30 @pytest.mark.usefixtures("testuser_api", "app")
31 class TestClosePullRequest(object):
31 class TestClosePullRequest(object):
32
32
33 @pytest.mark.backends("git", "hg")
33 @pytest.mark.backends("git", "hg")
34 def test_api_close_pull_request(self, pr_util):
34 def test_api_close_pull_request(self, pr_util):
35 pull_request = pr_util.create_pull_request()
35 pull_request = pr_util.create_pull_request()
36 pull_request_id = pull_request.pull_request_id
36 pull_request_id = pull_request.pull_request_id
37 author = pull_request.user_id
37 author = pull_request.user_id
38 repo = pull_request.target_repo.repo_id
38 repo = pull_request.target_repo.repo_id
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'close_pull_request',
40 self.apikey, 'close_pull_request',
41 repoid=pull_request.target_repo.repo_name,
41 repoid=pull_request.target_repo.repo_name,
42 pullrequestid=pull_request.pull_request_id)
42 pullrequestid=pull_request.pull_request_id)
43 response = api_call(self.app, params)
43 response = api_call(self.app, params)
44 expected = {
44 expected = {
45 'pull_request_id': pull_request_id,
45 'pull_request_id': pull_request_id,
46 'close_status': 'Rejected',
46 'closed': True,
47 'closed': True,
47 }
48 }
48 assert_ok(id_, expected, response.body)
49 assert_ok(id_, expected, response.body)
49 action = 'user_closed_pull_request:%d' % pull_request_id
50 action = 'user_closed_pull_request:%d' % pull_request_id
50 journal = UserLog.query()\
51 journal = UserLog.query()\
51 .filter(UserLog.user_id == author)\
52 .filter(UserLog.user_id == author)\
52 .filter(UserLog.repository_id == repo)\
53 .filter(UserLog.repository_id == repo)\
53 .filter(UserLog.action == action)\
54 .filter(UserLog.action == action)\
54 .all()
55 .all()
55 assert len(journal) == 1
56 assert len(journal) == 1
56
57
57 @pytest.mark.backends("git", "hg")
58 @pytest.mark.backends("git", "hg")
58 def test_api_close_pull_request_already_closed_error(self, pr_util):
59 def test_api_close_pull_request_already_closed_error(self, pr_util):
59 pull_request = pr_util.create_pull_request()
60 pull_request = pr_util.create_pull_request()
60 pull_request_id = pull_request.pull_request_id
61 pull_request_id = pull_request.pull_request_id
61 pull_request_repo = pull_request.target_repo.repo_name
62 pull_request_repo = pull_request.target_repo.repo_name
62 PullRequestModel().close_pull_request(
63 PullRequestModel().close_pull_request(
63 pull_request, pull_request.author)
64 pull_request, pull_request.author)
64 id_, params = build_data(
65 id_, params = build_data(
65 self.apikey, 'close_pull_request',
66 self.apikey, 'close_pull_request',
66 repoid=pull_request_repo, pullrequestid=pull_request_id)
67 repoid=pull_request_repo, pullrequestid=pull_request_id)
67 response = api_call(self.app, params)
68 response = api_call(self.app, params)
68
69
69 expected = 'pull request `%s` is already closed' % pull_request_id
70 expected = 'pull request `%s` is already closed' % pull_request_id
70 assert_error(id_, expected, given=response.body)
71 assert_error(id_, expected, given=response.body)
71
72
72 @pytest.mark.backends("git", "hg")
73 @pytest.mark.backends("git", "hg")
73 def test_api_close_pull_request_repo_error(self):
74 def test_api_close_pull_request_repo_error(self):
74 id_, params = build_data(
75 id_, params = build_data(
75 self.apikey, 'close_pull_request',
76 self.apikey, 'close_pull_request',
76 repoid=666, pullrequestid=1)
77 repoid=666, pullrequestid=1)
77 response = api_call(self.app, params)
78 response = api_call(self.app, params)
78
79
79 expected = 'repository `666` does not exist'
80 expected = 'repository `666` does not exist'
80 assert_error(id_, expected, given=response.body)
81 assert_error(id_, expected, given=response.body)
81
82
82 @pytest.mark.backends("git", "hg")
83 @pytest.mark.backends("git", "hg")
83 def test_api_close_pull_request_non_admin_with_userid_error(self,
84 def test_api_close_pull_request_non_admin_with_userid_error(self,
84 pr_util):
85 pr_util):
85 pull_request = pr_util.create_pull_request()
86 pull_request = pr_util.create_pull_request()
86 id_, params = build_data(
87 id_, params = build_data(
87 self.apikey_regular, 'close_pull_request',
88 self.apikey_regular, 'close_pull_request',
88 repoid=pull_request.target_repo.repo_name,
89 repoid=pull_request.target_repo.repo_name,
89 pullrequestid=pull_request.pull_request_id,
90 pullrequestid=pull_request.pull_request_id,
90 userid=TEST_USER_ADMIN_LOGIN)
91 userid=TEST_USER_ADMIN_LOGIN)
91 response = api_call(self.app, params)
92 response = api_call(self.app, params)
92
93
93 expected = 'userid is not the same as your user'
94 expected = 'userid is not the same as your user'
94 assert_error(id_, expected, given=response.body)
95 assert_error(id_, expected, given=response.body)
95
96
96 @pytest.mark.backends("git", "hg")
97 @pytest.mark.backends("git", "hg")
97 def test_api_close_pull_request_no_perms_to_close(
98 def test_api_close_pull_request_no_perms_to_close(
98 self, user_util, pr_util):
99 self, user_util, pr_util):
99 user = user_util.create_user()
100 user = user_util.create_user()
100 pull_request = pr_util.create_pull_request()
101 pull_request = pr_util.create_pull_request()
101
102
102 id_, params = build_data(
103 id_, params = build_data(
103 user.api_key, 'close_pull_request',
104 user.api_key, 'close_pull_request',
104 repoid=pull_request.target_repo.repo_name,
105 repoid=pull_request.target_repo.repo_name,
105 pullrequestid=pull_request.pull_request_id,)
106 pullrequestid=pull_request.pull_request_id,)
106 response = api_call(self.app, params)
107 response = api_call(self.app, params)
107
108
108 expected = ('pull request `%s` close failed, '
109 expected = ('pull request `%s` close failed, '
109 'no permission to close.') % pull_request.pull_request_id
110 'no permission to close.') % pull_request.pull_request_id
110
111
111 response_json = response.json['error']
112 response_json = response.json['error']
112 assert response_json == expected
113 assert response_json == expected
@@ -1,734 +1,748 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 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 import logging
22 import logging
23
23
24 from rhodecode import events
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 validate_repo_permissions, resolve_ref_or_error)
29 validate_repo_permissions, resolve_ref_or_error)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema.schemas.reviewer_schema import \
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
39 ReviewerListSchema
40 ReviewerListSchema)
40
41
41 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
42
43
43
44
44 @jsonrpc_method()
45 @jsonrpc_method()
45 def get_pull_request(request, apiuser, repoid, pullrequestid):
46 def get_pull_request(request, apiuser, repoid, pullrequestid):
46 """
47 """
47 Get a pull request based on the given ID.
48 Get a pull request based on the given ID.
48
49
49 :param apiuser: This is filled automatically from the |authtoken|.
50 :param apiuser: This is filled automatically from the |authtoken|.
50 :type apiuser: AuthUser
51 :type apiuser: AuthUser
51 :param repoid: Repository name or repository ID from where the pull
52 :param repoid: Repository name or repository ID from where the pull
52 request was opened.
53 request was opened.
53 :type repoid: str or int
54 :type repoid: str or int
54 :param pullrequestid: ID of the requested pull request.
55 :param pullrequestid: ID of the requested pull request.
55 :type pullrequestid: int
56 :type pullrequestid: int
56
57
57 Example output:
58 Example output:
58
59
59 .. code-block:: bash
60 .. code-block:: bash
60
61
61 "id": <id_given_in_input>,
62 "id": <id_given_in_input>,
62 "result":
63 "result":
63 {
64 {
64 "pull_request_id": "<pull_request_id>",
65 "pull_request_id": "<pull_request_id>",
65 "url": "<url>",
66 "url": "<url>",
66 "title": "<title>",
67 "title": "<title>",
67 "description": "<description>",
68 "description": "<description>",
68 "status" : "<status>",
69 "status" : "<status>",
69 "created_on": "<date_time_created>",
70 "created_on": "<date_time_created>",
70 "updated_on": "<date_time_updated>",
71 "updated_on": "<date_time_updated>",
71 "commit_ids": [
72 "commit_ids": [
72 ...
73 ...
73 "<commit_id>",
74 "<commit_id>",
74 "<commit_id>",
75 "<commit_id>",
75 ...
76 ...
76 ],
77 ],
77 "review_status": "<review_status>",
78 "review_status": "<review_status>",
78 "mergeable": {
79 "mergeable": {
79 "status": "<bool>",
80 "status": "<bool>",
80 "message": "<message>",
81 "message": "<message>",
81 },
82 },
82 "source": {
83 "source": {
83 "clone_url": "<clone_url>",
84 "clone_url": "<clone_url>",
84 "repository": "<repository_name>",
85 "repository": "<repository_name>",
85 "reference":
86 "reference":
86 {
87 {
87 "name": "<name>",
88 "name": "<name>",
88 "type": "<type>",
89 "type": "<type>",
89 "commit_id": "<commit_id>",
90 "commit_id": "<commit_id>",
90 }
91 }
91 },
92 },
92 "target": {
93 "target": {
93 "clone_url": "<clone_url>",
94 "clone_url": "<clone_url>",
94 "repository": "<repository_name>",
95 "repository": "<repository_name>",
95 "reference":
96 "reference":
96 {
97 {
97 "name": "<name>",
98 "name": "<name>",
98 "type": "<type>",
99 "type": "<type>",
99 "commit_id": "<commit_id>",
100 "commit_id": "<commit_id>",
100 }
101 }
101 },
102 },
102 "merge": {
103 "merge": {
103 "clone_url": "<clone_url>",
104 "clone_url": "<clone_url>",
104 "reference":
105 "reference":
105 {
106 {
106 "name": "<name>",
107 "name": "<name>",
107 "type": "<type>",
108 "type": "<type>",
108 "commit_id": "<commit_id>",
109 "commit_id": "<commit_id>",
109 }
110 }
110 },
111 },
111 "author": <user_obj>,
112 "author": <user_obj>,
112 "reviewers": [
113 "reviewers": [
113 ...
114 ...
114 {
115 {
115 "user": "<user_obj>",
116 "user": "<user_obj>",
116 "review_status": "<review_status>",
117 "review_status": "<review_status>",
117 }
118 }
118 ...
119 ...
119 ]
120 ]
120 },
121 },
121 "error": null
122 "error": null
122 """
123 """
123 get_repo_or_error(repoid)
124 get_repo_or_error(repoid)
124 pull_request = get_pull_request_or_error(pullrequestid)
125 pull_request = get_pull_request_or_error(pullrequestid)
125 if not PullRequestModel().check_user_read(
126 if not PullRequestModel().check_user_read(
126 pull_request, apiuser, api=True):
127 pull_request, apiuser, api=True):
127 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
128 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
128 data = pull_request.get_api_data()
129 data = pull_request.get_api_data()
129 return data
130 return data
130
131
131
132
132 @jsonrpc_method()
133 @jsonrpc_method()
133 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
134 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
134 """
135 """
135 Get all pull requests from the repository specified in `repoid`.
136 Get all pull requests from the repository specified in `repoid`.
136
137
137 :param apiuser: This is filled automatically from the |authtoken|.
138 :param apiuser: This is filled automatically from the |authtoken|.
138 :type apiuser: AuthUser
139 :type apiuser: AuthUser
139 :param repoid: Repository name or repository ID.
140 :param repoid: Repository name or repository ID.
140 :type repoid: str or int
141 :type repoid: str or int
141 :param status: Only return pull requests with the specified status.
142 :param status: Only return pull requests with the specified status.
142 Valid options are.
143 Valid options are.
143 * ``new`` (default)
144 * ``new`` (default)
144 * ``open``
145 * ``open``
145 * ``closed``
146 * ``closed``
146 :type status: str
147 :type status: str
147
148
148 Example output:
149 Example output:
149
150
150 .. code-block:: bash
151 .. code-block:: bash
151
152
152 "id": <id_given_in_input>,
153 "id": <id_given_in_input>,
153 "result":
154 "result":
154 [
155 [
155 ...
156 ...
156 {
157 {
157 "pull_request_id": "<pull_request_id>",
158 "pull_request_id": "<pull_request_id>",
158 "url": "<url>",
159 "url": "<url>",
159 "title" : "<title>",
160 "title" : "<title>",
160 "description": "<description>",
161 "description": "<description>",
161 "status": "<status>",
162 "status": "<status>",
162 "created_on": "<date_time_created>",
163 "created_on": "<date_time_created>",
163 "updated_on": "<date_time_updated>",
164 "updated_on": "<date_time_updated>",
164 "commit_ids": [
165 "commit_ids": [
165 ...
166 ...
166 "<commit_id>",
167 "<commit_id>",
167 "<commit_id>",
168 "<commit_id>",
168 ...
169 ...
169 ],
170 ],
170 "review_status": "<review_status>",
171 "review_status": "<review_status>",
171 "mergeable": {
172 "mergeable": {
172 "status": "<bool>",
173 "status": "<bool>",
173 "message: "<message>",
174 "message: "<message>",
174 },
175 },
175 "source": {
176 "source": {
176 "clone_url": "<clone_url>",
177 "clone_url": "<clone_url>",
177 "reference":
178 "reference":
178 {
179 {
179 "name": "<name>",
180 "name": "<name>",
180 "type": "<type>",
181 "type": "<type>",
181 "commit_id": "<commit_id>",
182 "commit_id": "<commit_id>",
182 }
183 }
183 },
184 },
184 "target": {
185 "target": {
185 "clone_url": "<clone_url>",
186 "clone_url": "<clone_url>",
186 "reference":
187 "reference":
187 {
188 {
188 "name": "<name>",
189 "name": "<name>",
189 "type": "<type>",
190 "type": "<type>",
190 "commit_id": "<commit_id>",
191 "commit_id": "<commit_id>",
191 }
192 }
192 },
193 },
193 "merge": {
194 "merge": {
194 "clone_url": "<clone_url>",
195 "clone_url": "<clone_url>",
195 "reference":
196 "reference":
196 {
197 {
197 "name": "<name>",
198 "name": "<name>",
198 "type": "<type>",
199 "type": "<type>",
199 "commit_id": "<commit_id>",
200 "commit_id": "<commit_id>",
200 }
201 }
201 },
202 },
202 "author": <user_obj>,
203 "author": <user_obj>,
203 "reviewers": [
204 "reviewers": [
204 ...
205 ...
205 {
206 {
206 "user": "<user_obj>",
207 "user": "<user_obj>",
207 "review_status": "<review_status>",
208 "review_status": "<review_status>",
208 }
209 }
209 ...
210 ...
210 ]
211 ]
211 }
212 }
212 ...
213 ...
213 ],
214 ],
214 "error": null
215 "error": null
215
216
216 """
217 """
217 repo = get_repo_or_error(repoid)
218 repo = get_repo_or_error(repoid)
218 if not has_superadmin_permission(apiuser):
219 if not has_superadmin_permission(apiuser):
219 _perms = (
220 _perms = (
220 'repository.admin', 'repository.write', 'repository.read',)
221 'repository.admin', 'repository.write', 'repository.read',)
221 validate_repo_permissions(apiuser, repoid, repo, _perms)
222 validate_repo_permissions(apiuser, repoid, repo, _perms)
222
223
223 status = Optional.extract(status)
224 status = Optional.extract(status)
224 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
225 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
225 data = [pr.get_api_data() for pr in pull_requests]
226 data = [pr.get_api_data() for pr in pull_requests]
226 return data
227 return data
227
228
228
229
229 @jsonrpc_method()
230 @jsonrpc_method()
230 def merge_pull_request(request, apiuser, repoid, pullrequestid,
231 def merge_pull_request(
231 userid=Optional(OAttr('apiuser'))):
232 request, apiuser, repoid, pullrequestid,
233 userid=Optional(OAttr('apiuser'))):
232 """
234 """
233 Merge the pull request specified by `pullrequestid` into its target
235 Merge the pull request specified by `pullrequestid` into its target
234 repository.
236 repository.
235
237
236 :param apiuser: This is filled automatically from the |authtoken|.
238 :param apiuser: This is filled automatically from the |authtoken|.
237 :type apiuser: AuthUser
239 :type apiuser: AuthUser
238 :param repoid: The Repository name or repository ID of the
240 :param repoid: The Repository name or repository ID of the
239 target repository to which the |pr| is to be merged.
241 target repository to which the |pr| is to be merged.
240 :type repoid: str or int
242 :type repoid: str or int
241 :param pullrequestid: ID of the pull request which shall be merged.
243 :param pullrequestid: ID of the pull request which shall be merged.
242 :type pullrequestid: int
244 :type pullrequestid: int
243 :param userid: Merge the pull request as this user.
245 :param userid: Merge the pull request as this user.
244 :type userid: Optional(str or int)
246 :type userid: Optional(str or int)
245
247
246 Example output:
248 Example output:
247
249
248 .. code-block:: bash
250 .. code-block:: bash
249
251
250 "id": <id_given_in_input>,
252 "id": <id_given_in_input>,
251 "result": {
253 "result": {
252 "executed": "<bool>",
254 "executed": "<bool>",
253 "failure_reason": "<int>",
255 "failure_reason": "<int>",
254 "merge_commit_id": "<merge_commit_id>",
256 "merge_commit_id": "<merge_commit_id>",
255 "possible": "<bool>",
257 "possible": "<bool>",
256 "merge_ref": {
258 "merge_ref": {
257 "commit_id": "<commit_id>",
259 "commit_id": "<commit_id>",
258 "type": "<type>",
260 "type": "<type>",
259 "name": "<name>"
261 "name": "<name>"
260 }
262 }
261 },
263 },
262 "error": null
264 "error": null
263 """
265 """
264 repo = get_repo_or_error(repoid)
266 repo = get_repo_or_error(repoid)
265 if not isinstance(userid, Optional):
267 if not isinstance(userid, Optional):
266 if (has_superadmin_permission(apiuser) or
268 if (has_superadmin_permission(apiuser) or
267 HasRepoPermissionAnyApi('repository.admin')(
269 HasRepoPermissionAnyApi('repository.admin')(
268 user=apiuser, repo_name=repo.repo_name)):
270 user=apiuser, repo_name=repo.repo_name)):
269 apiuser = get_user_or_error(userid)
271 apiuser = get_user_or_error(userid)
270 else:
272 else:
271 raise JSONRPCError('userid is not the same as your user')
273 raise JSONRPCError('userid is not the same as your user')
272
274
273 pull_request = get_pull_request_or_error(pullrequestid)
275 pull_request = get_pull_request_or_error(pullrequestid)
274
276
275 check = MergeCheck.validate(pull_request, user=apiuser)
277 check = MergeCheck.validate(pull_request, user=apiuser)
276 merge_possible = not check.failed
278 merge_possible = not check.failed
277
279
278 if not merge_possible:
280 if not merge_possible:
279 error_messages = []
281 error_messages = []
280 for err_type, error_msg in check.errors:
282 for err_type, error_msg in check.errors:
281 error_msg = request.translate(error_msg)
283 error_msg = request.translate(error_msg)
282 error_messages.append(error_msg)
284 error_messages.append(error_msg)
283
285
284 reasons = ','.join(error_messages)
286 reasons = ','.join(error_messages)
285 raise JSONRPCError(
287 raise JSONRPCError(
286 'merge not possible for following reasons: {}'.format(reasons))
288 'merge not possible for following reasons: {}'.format(reasons))
287
289
288 target_repo = pull_request.target_repo
290 target_repo = pull_request.target_repo
289 extras = vcs_operation_context(
291 extras = vcs_operation_context(
290 request.environ, repo_name=target_repo.repo_name,
292 request.environ, repo_name=target_repo.repo_name,
291 username=apiuser.username, action='push',
293 username=apiuser.username, action='push',
292 scm=target_repo.repo_type)
294 scm=target_repo.repo_type)
293 merge_response = PullRequestModel().merge(
295 merge_response = PullRequestModel().merge(
294 pull_request, apiuser, extras=extras)
296 pull_request, apiuser, extras=extras)
295 if merge_response.executed:
297 if merge_response.executed:
296 PullRequestModel().close_pull_request(
298 PullRequestModel().close_pull_request(
297 pull_request.pull_request_id, apiuser)
299 pull_request.pull_request_id, apiuser)
298
300
299 Session().commit()
301 Session().commit()
300
302
301 # In previous versions the merge response directly contained the merge
303 # In previous versions the merge response directly contained the merge
302 # commit id. It is now contained in the merge reference object. To be
304 # commit id. It is now contained in the merge reference object. To be
303 # backwards compatible we have to extract it again.
305 # backwards compatible we have to extract it again.
304 merge_response = merge_response._asdict()
306 merge_response = merge_response._asdict()
305 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
307 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
306
308
307 return merge_response
309 return merge_response
308
310
309
311
310 @jsonrpc_method()
312 @jsonrpc_method()
311 def close_pull_request(request, apiuser, repoid, pullrequestid,
312 userid=Optional(OAttr('apiuser'))):
313 """
314 Close the pull request specified by `pullrequestid`.
315
316 :param apiuser: This is filled automatically from the |authtoken|.
317 :type apiuser: AuthUser
318 :param repoid: Repository name or repository ID to which the pull
319 request belongs.
320 :type repoid: str or int
321 :param pullrequestid: ID of the pull request to be closed.
322 :type pullrequestid: int
323 :param userid: Close the pull request as this user.
324 :type userid: Optional(str or int)
325
326 Example output:
327
328 .. code-block:: bash
329
330 "id": <id_given_in_input>,
331 "result": {
332 "pull_request_id": "<int>",
333 "closed": "<bool>"
334 },
335 "error": null
336
337 """
338 repo = get_repo_or_error(repoid)
339 if not isinstance(userid, Optional):
340 if (has_superadmin_permission(apiuser) or
341 HasRepoPermissionAnyApi('repository.admin')(
342 user=apiuser, repo_name=repo.repo_name)):
343 apiuser = get_user_or_error(userid)
344 else:
345 raise JSONRPCError('userid is not the same as your user')
346
347 pull_request = get_pull_request_or_error(pullrequestid)
348 if not PullRequestModel().check_user_update(
349 pull_request, apiuser, api=True):
350 raise JSONRPCError(
351 'pull request `%s` close failed, no permission to close.' % (
352 pullrequestid,))
353 if pull_request.is_closed():
354 raise JSONRPCError(
355 'pull request `%s` is already closed' % (pullrequestid,))
356
357 PullRequestModel().close_pull_request(
358 pull_request.pull_request_id, apiuser)
359 Session().commit()
360 data = {
361 'pull_request_id': pull_request.pull_request_id,
362 'closed': True,
363 }
364 return data
365
366
367 @jsonrpc_method()
368 def comment_pull_request(
313 def comment_pull_request(
369 request, apiuser, repoid, pullrequestid, message=Optional(None),
314 request, apiuser, repoid, pullrequestid, message=Optional(None),
370 commit_id=Optional(None), status=Optional(None),
315 commit_id=Optional(None), status=Optional(None),
371 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
316 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
372 resolves_comment_id=Optional(None),
317 resolves_comment_id=Optional(None),
373 userid=Optional(OAttr('apiuser'))):
318 userid=Optional(OAttr('apiuser'))):
374 """
319 """
375 Comment on the pull request specified with the `pullrequestid`,
320 Comment on the pull request specified with the `pullrequestid`,
376 in the |repo| specified by the `repoid`, and optionally change the
321 in the |repo| specified by the `repoid`, and optionally change the
377 review status.
322 review status.
378
323
379 :param apiuser: This is filled automatically from the |authtoken|.
324 :param apiuser: This is filled automatically from the |authtoken|.
380 :type apiuser: AuthUser
325 :type apiuser: AuthUser
381 :param repoid: The repository name or repository ID.
326 :param repoid: The repository name or repository ID.
382 :type repoid: str or int
327 :type repoid: str or int
383 :param pullrequestid: The pull request ID.
328 :param pullrequestid: The pull request ID.
384 :type pullrequestid: int
329 :type pullrequestid: int
385 :param commit_id: Specify the commit_id for which to set a comment. If
330 :param commit_id: Specify the commit_id for which to set a comment. If
386 given commit_id is different than latest in the PR status
331 given commit_id is different than latest in the PR status
387 change won't be performed.
332 change won't be performed.
388 :type commit_id: str
333 :type commit_id: str
389 :param message: The text content of the comment.
334 :param message: The text content of the comment.
390 :type message: str
335 :type message: str
391 :param status: (**Optional**) Set the approval status of the pull
336 :param status: (**Optional**) Set the approval status of the pull
392 request. One of: 'not_reviewed', 'approved', 'rejected',
337 request. One of: 'not_reviewed', 'approved', 'rejected',
393 'under_review'
338 'under_review'
394 :type status: str
339 :type status: str
395 :param comment_type: Comment type, one of: 'note', 'todo'
340 :param comment_type: Comment type, one of: 'note', 'todo'
396 :type comment_type: Optional(str), default: 'note'
341 :type comment_type: Optional(str), default: 'note'
397 :param userid: Comment on the pull request as this user
342 :param userid: Comment on the pull request as this user
398 :type userid: Optional(str or int)
343 :type userid: Optional(str or int)
399
344
400 Example output:
345 Example output:
401
346
402 .. code-block:: bash
347 .. code-block:: bash
403
348
404 id : <id_given_in_input>
349 id : <id_given_in_input>
405 result : {
350 result : {
406 "pull_request_id": "<Integer>",
351 "pull_request_id": "<Integer>",
407 "comment_id": "<Integer>",
352 "comment_id": "<Integer>",
408 "status": {"given": <given_status>,
353 "status": {"given": <given_status>,
409 "was_changed": <bool status_was_actually_changed> },
354 "was_changed": <bool status_was_actually_changed> },
410 },
355 },
411 error : null
356 error : null
412 """
357 """
413 repo = get_repo_or_error(repoid)
358 repo = get_repo_or_error(repoid)
414 if not isinstance(userid, Optional):
359 if not isinstance(userid, Optional):
415 if (has_superadmin_permission(apiuser) or
360 if (has_superadmin_permission(apiuser) or
416 HasRepoPermissionAnyApi('repository.admin')(
361 HasRepoPermissionAnyApi('repository.admin')(
417 user=apiuser, repo_name=repo.repo_name)):
362 user=apiuser, repo_name=repo.repo_name)):
418 apiuser = get_user_or_error(userid)
363 apiuser = get_user_or_error(userid)
419 else:
364 else:
420 raise JSONRPCError('userid is not the same as your user')
365 raise JSONRPCError('userid is not the same as your user')
421
366
422 pull_request = get_pull_request_or_error(pullrequestid)
367 pull_request = get_pull_request_or_error(pullrequestid)
423 if not PullRequestModel().check_user_read(
368 if not PullRequestModel().check_user_read(
424 pull_request, apiuser, api=True):
369 pull_request, apiuser, api=True):
425 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
370 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
426 message = Optional.extract(message)
371 message = Optional.extract(message)
427 status = Optional.extract(status)
372 status = Optional.extract(status)
428 commit_id = Optional.extract(commit_id)
373 commit_id = Optional.extract(commit_id)
429 comment_type = Optional.extract(comment_type)
374 comment_type = Optional.extract(comment_type)
430 resolves_comment_id = Optional.extract(resolves_comment_id)
375 resolves_comment_id = Optional.extract(resolves_comment_id)
431
376
432 if not message and not status:
377 if not message and not status:
433 raise JSONRPCError(
378 raise JSONRPCError(
434 'Both message and status parameters are missing. '
379 'Both message and status parameters are missing. '
435 'At least one is required.')
380 'At least one is required.')
436
381
437 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
382 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
438 status is not None):
383 status is not None):
439 raise JSONRPCError('Unknown comment status: `%s`' % status)
384 raise JSONRPCError('Unknown comment status: `%s`' % status)
440
385
441 if commit_id and commit_id not in pull_request.revisions:
386 if commit_id and commit_id not in pull_request.revisions:
442 raise JSONRPCError(
387 raise JSONRPCError(
443 'Invalid commit_id `%s` for this pull request.' % commit_id)
388 'Invalid commit_id `%s` for this pull request.' % commit_id)
444
389
445 allowed_to_change_status = PullRequestModel().check_user_change_status(
390 allowed_to_change_status = PullRequestModel().check_user_change_status(
446 pull_request, apiuser)
391 pull_request, apiuser)
447
392
448 # if commit_id is passed re-validated if user is allowed to change status
393 # if commit_id is passed re-validated if user is allowed to change status
449 # based on latest commit_id from the PR
394 # based on latest commit_id from the PR
450 if commit_id:
395 if commit_id:
451 commit_idx = pull_request.revisions.index(commit_id)
396 commit_idx = pull_request.revisions.index(commit_id)
452 if commit_idx != 0:
397 if commit_idx != 0:
453 allowed_to_change_status = False
398 allowed_to_change_status = False
454
399
455 if resolves_comment_id:
400 if resolves_comment_id:
456 comment = ChangesetComment.get(resolves_comment_id)
401 comment = ChangesetComment.get(resolves_comment_id)
457 if not comment:
402 if not comment:
458 raise JSONRPCError(
403 raise JSONRPCError(
459 'Invalid resolves_comment_id `%s` for this pull request.'
404 'Invalid resolves_comment_id `%s` for this pull request.'
460 % resolves_comment_id)
405 % resolves_comment_id)
461 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
406 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
462 raise JSONRPCError(
407 raise JSONRPCError(
463 'Comment `%s` is wrong type for setting status to resolved.'
408 'Comment `%s` is wrong type for setting status to resolved.'
464 % resolves_comment_id)
409 % resolves_comment_id)
465
410
466 text = message
411 text = message
467 status_label = ChangesetStatus.get_status_lbl(status)
412 status_label = ChangesetStatus.get_status_lbl(status)
468 if status and allowed_to_change_status:
413 if status and allowed_to_change_status:
469 st_message = ('Status change %(transition_icon)s %(status)s'
414 st_message = ('Status change %(transition_icon)s %(status)s'
470 % {'transition_icon': '>', 'status': status_label})
415 % {'transition_icon': '>', 'status': status_label})
471 text = message or st_message
416 text = message or st_message
472
417
473 rc_config = SettingsModel().get_all_settings()
418 rc_config = SettingsModel().get_all_settings()
474 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
419 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
475
420
476 status_change = status and allowed_to_change_status
421 status_change = status and allowed_to_change_status
477 comment = CommentsModel().create(
422 comment = CommentsModel().create(
478 text=text,
423 text=text,
479 repo=pull_request.target_repo.repo_id,
424 repo=pull_request.target_repo.repo_id,
480 user=apiuser.user_id,
425 user=apiuser.user_id,
481 pull_request=pull_request.pull_request_id,
426 pull_request=pull_request.pull_request_id,
482 f_path=None,
427 f_path=None,
483 line_no=None,
428 line_no=None,
484 status_change=(status_label if status_change else None),
429 status_change=(status_label if status_change else None),
485 status_change_type=(status if status_change else None),
430 status_change_type=(status if status_change else None),
486 closing_pr=False,
431 closing_pr=False,
487 renderer=renderer,
432 renderer=renderer,
488 comment_type=comment_type,
433 comment_type=comment_type,
489 resolves_comment_id=resolves_comment_id
434 resolves_comment_id=resolves_comment_id
490 )
435 )
491
436
492 if allowed_to_change_status and status:
437 if allowed_to_change_status and status:
493 ChangesetStatusModel().set_status(
438 ChangesetStatusModel().set_status(
494 pull_request.target_repo.repo_id,
439 pull_request.target_repo.repo_id,
495 status,
440 status,
496 apiuser.user_id,
441 apiuser.user_id,
497 comment,
442 comment,
498 pull_request=pull_request.pull_request_id
443 pull_request=pull_request.pull_request_id
499 )
444 )
500 Session().flush()
445 Session().flush()
501
446
502 Session().commit()
447 Session().commit()
503 data = {
448 data = {
504 'pull_request_id': pull_request.pull_request_id,
449 'pull_request_id': pull_request.pull_request_id,
505 'comment_id': comment.comment_id if comment else None,
450 'comment_id': comment.comment_id if comment else None,
506 'status': {'given': status, 'was_changed': status_change},
451 'status': {'given': status, 'was_changed': status_change},
507 }
452 }
508 return data
453 return data
509
454
510
455
511 @jsonrpc_method()
456 @jsonrpc_method()
512 def create_pull_request(
457 def create_pull_request(
513 request, apiuser, source_repo, target_repo, source_ref, target_ref,
458 request, apiuser, source_repo, target_repo, source_ref, target_ref,
514 title, description=Optional(''), reviewers=Optional(None)):
459 title, description=Optional(''), reviewers=Optional(None)):
515 """
460 """
516 Creates a new pull request.
461 Creates a new pull request.
517
462
518 Accepts refs in the following formats:
463 Accepts refs in the following formats:
519
464
520 * branch:<branch_name>:<sha>
465 * branch:<branch_name>:<sha>
521 * branch:<branch_name>
466 * branch:<branch_name>
522 * bookmark:<bookmark_name>:<sha> (Mercurial only)
467 * bookmark:<bookmark_name>:<sha> (Mercurial only)
523 * bookmark:<bookmark_name> (Mercurial only)
468 * bookmark:<bookmark_name> (Mercurial only)
524
469
525 :param apiuser: This is filled automatically from the |authtoken|.
470 :param apiuser: This is filled automatically from the |authtoken|.
526 :type apiuser: AuthUser
471 :type apiuser: AuthUser
527 :param source_repo: Set the source repository name.
472 :param source_repo: Set the source repository name.
528 :type source_repo: str
473 :type source_repo: str
529 :param target_repo: Set the target repository name.
474 :param target_repo: Set the target repository name.
530 :type target_repo: str
475 :type target_repo: str
531 :param source_ref: Set the source ref name.
476 :param source_ref: Set the source ref name.
532 :type source_ref: str
477 :type source_ref: str
533 :param target_ref: Set the target ref name.
478 :param target_ref: Set the target ref name.
534 :type target_ref: str
479 :type target_ref: str
535 :param title: Set the pull request title.
480 :param title: Set the pull request title.
536 :type title: str
481 :type title: str
537 :param description: Set the pull request description.
482 :param description: Set the pull request description.
538 :type description: Optional(str)
483 :type description: Optional(str)
539 :param reviewers: Set the new pull request reviewers list.
484 :param reviewers: Set the new pull request reviewers list.
540 :type reviewers: Optional(list)
485 :type reviewers: Optional(list)
541 Accepts username strings or objects of the format:
486 Accepts username strings or objects of the format:
542
487
543 {'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}
488 {'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}
544 """
489 """
545
490
546 source = get_repo_or_error(source_repo)
491 source = get_repo_or_error(source_repo)
547 target = get_repo_or_error(target_repo)
492 target = get_repo_or_error(target_repo)
548 if not has_superadmin_permission(apiuser):
493 if not has_superadmin_permission(apiuser):
549 _perms = ('repository.admin', 'repository.write', 'repository.read',)
494 _perms = ('repository.admin', 'repository.write', 'repository.read',)
550 validate_repo_permissions(apiuser, source_repo, source, _perms)
495 validate_repo_permissions(apiuser, source_repo, source, _perms)
551
496
552 full_source_ref = resolve_ref_or_error(source_ref, source)
497 full_source_ref = resolve_ref_or_error(source_ref, source)
553 full_target_ref = resolve_ref_or_error(target_ref, target)
498 full_target_ref = resolve_ref_or_error(target_ref, target)
554 source_commit = get_commit_or_error(full_source_ref, source)
499 source_commit = get_commit_or_error(full_source_ref, source)
555 target_commit = get_commit_or_error(full_target_ref, target)
500 target_commit = get_commit_or_error(full_target_ref, target)
556 source_scm = source.scm_instance()
501 source_scm = source.scm_instance()
557 target_scm = target.scm_instance()
502 target_scm = target.scm_instance()
558
503
559 commit_ranges = target_scm.compare(
504 commit_ranges = target_scm.compare(
560 target_commit.raw_id, source_commit.raw_id, source_scm,
505 target_commit.raw_id, source_commit.raw_id, source_scm,
561 merge=True, pre_load=[])
506 merge=True, pre_load=[])
562
507
563 ancestor = target_scm.get_common_ancestor(
508 ancestor = target_scm.get_common_ancestor(
564 target_commit.raw_id, source_commit.raw_id, source_scm)
509 target_commit.raw_id, source_commit.raw_id, source_scm)
565
510
566 if not commit_ranges:
511 if not commit_ranges:
567 raise JSONRPCError('no commits found')
512 raise JSONRPCError('no commits found')
568
513
569 if not ancestor:
514 if not ancestor:
570 raise JSONRPCError('no common ancestor found')
515 raise JSONRPCError('no common ancestor found')
571
516
572 reviewer_objects = Optional.extract(reviewers) or []
517 reviewer_objects = Optional.extract(reviewers) or []
573 if reviewer_objects:
518 if reviewer_objects:
574 schema = ReviewerListSchema()
519 schema = ReviewerListSchema()
575 try:
520 try:
576 reviewer_objects = schema.deserialize(reviewer_objects)
521 reviewer_objects = schema.deserialize(reviewer_objects)
577 except Invalid as err:
522 except Invalid as err:
578 raise JSONRPCValidationError(colander_exc=err)
523 raise JSONRPCValidationError(colander_exc=err)
579
524
580 reviewers = []
525 reviewers = []
581 for reviewer_object in reviewer_objects:
526 for reviewer_object in reviewer_objects:
582 user = get_user_or_error(reviewer_object['username'])
527 user = get_user_or_error(reviewer_object['username'])
583 reasons = reviewer_object['reasons']
528 reasons = reviewer_object['reasons']
584 mandatory = reviewer_object['mandatory']
529 mandatory = reviewer_object['mandatory']
585 reviewers.append((user.user_id, reasons, mandatory))
530 reviewers.append((user.user_id, reasons, mandatory))
586
531
587 pull_request_model = PullRequestModel()
532 pull_request_model = PullRequestModel()
588 pull_request = pull_request_model.create(
533 pull_request = pull_request_model.create(
589 created_by=apiuser.user_id,
534 created_by=apiuser.user_id,
590 source_repo=source_repo,
535 source_repo=source_repo,
591 source_ref=full_source_ref,
536 source_ref=full_source_ref,
592 target_repo=target_repo,
537 target_repo=target_repo,
593 target_ref=full_target_ref,
538 target_ref=full_target_ref,
594 revisions=reversed(
539 revisions=reversed(
595 [commit.raw_id for commit in reversed(commit_ranges)]),
540 [commit.raw_id for commit in reversed(commit_ranges)]),
596 reviewers=reviewers,
541 reviewers=reviewers,
597 title=title,
542 title=title,
598 description=Optional.extract(description)
543 description=Optional.extract(description)
599 )
544 )
600
545
601 Session().commit()
546 Session().commit()
602 data = {
547 data = {
603 'msg': 'Created new pull request `{}`'.format(title),
548 'msg': 'Created new pull request `{}`'.format(title),
604 'pull_request_id': pull_request.pull_request_id,
549 'pull_request_id': pull_request.pull_request_id,
605 }
550 }
606 return data
551 return data
607
552
608
553
609 @jsonrpc_method()
554 @jsonrpc_method()
610 def update_pull_request(
555 def update_pull_request(
611 request, apiuser, repoid, pullrequestid, title=Optional(''),
556 request, apiuser, repoid, pullrequestid, title=Optional(''),
612 description=Optional(''), reviewers=Optional(None),
557 description=Optional(''), reviewers=Optional(None),
613 update_commits=Optional(None), close_pull_request=Optional(None)):
558 update_commits=Optional(None)):
614 """
559 """
615 Updates a pull request.
560 Updates a pull request.
616
561
617 :param apiuser: This is filled automatically from the |authtoken|.
562 :param apiuser: This is filled automatically from the |authtoken|.
618 :type apiuser: AuthUser
563 :type apiuser: AuthUser
619 :param repoid: The repository name or repository ID.
564 :param repoid: The repository name or repository ID.
620 :type repoid: str or int
565 :type repoid: str or int
621 :param pullrequestid: The pull request ID.
566 :param pullrequestid: The pull request ID.
622 :type pullrequestid: int
567 :type pullrequestid: int
623 :param title: Set the pull request title.
568 :param title: Set the pull request title.
624 :type title: str
569 :type title: str
625 :param description: Update pull request description.
570 :param description: Update pull request description.
626 :type description: Optional(str)
571 :type description: Optional(str)
627 :param reviewers: Update pull request reviewers list with new value.
572 :param reviewers: Update pull request reviewers list with new value.
628 :type reviewers: Optional(list)
573 :type reviewers: Optional(list)
629 Accepts username strings or objects of the format:
574 Accepts username strings or objects of the format:
630
575
631 {'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}
576 {'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}
632
577
633 :param update_commits: Trigger update of commits for this pull request
578 :param update_commits: Trigger update of commits for this pull request
634 :type: update_commits: Optional(bool)
579 :type: update_commits: Optional(bool)
635 :param close_pull_request: Close this pull request with rejected state
636 :type: close_pull_request: Optional(bool)
637
580
638 Example output:
581 Example output:
639
582
640 .. code-block:: bash
583 .. code-block:: bash
641
584
642 id : <id_given_in_input>
585 id : <id_given_in_input>
643 result : {
586 result : {
644 "msg": "Updated pull request `63`",
587 "msg": "Updated pull request `63`",
645 "pull_request": <pull_request_object>,
588 "pull_request": <pull_request_object>,
646 "updated_reviewers": {
589 "updated_reviewers": {
647 "added": [
590 "added": [
648 "username"
591 "username"
649 ],
592 ],
650 "removed": []
593 "removed": []
651 },
594 },
652 "updated_commits": {
595 "updated_commits": {
653 "added": [
596 "added": [
654 "<sha1_hash>"
597 "<sha1_hash>"
655 ],
598 ],
656 "common": [
599 "common": [
657 "<sha1_hash>",
600 "<sha1_hash>",
658 "<sha1_hash>",
601 "<sha1_hash>",
659 ],
602 ],
660 "removed": []
603 "removed": []
661 }
604 }
662 }
605 }
663 error : null
606 error : null
664 """
607 """
665
608
666 repo = get_repo_or_error(repoid)
609 repo = get_repo_or_error(repoid)
667 pull_request = get_pull_request_or_error(pullrequestid)
610 pull_request = get_pull_request_or_error(pullrequestid)
668 if not PullRequestModel().check_user_update(
611 if not PullRequestModel().check_user_update(
669 pull_request, apiuser, api=True):
612 pull_request, apiuser, api=True):
670 raise JSONRPCError(
613 raise JSONRPCError(
671 'pull request `%s` update failed, no permission to update.' % (
614 'pull request `%s` update failed, no permission to update.' % (
672 pullrequestid,))
615 pullrequestid,))
673 if pull_request.is_closed():
616 if pull_request.is_closed():
674 raise JSONRPCError(
617 raise JSONRPCError(
675 'pull request `%s` update failed, pull request is closed' % (
618 'pull request `%s` update failed, pull request is closed' % (
676 pullrequestid,))
619 pullrequestid,))
677
620
678
679 reviewer_objects = Optional.extract(reviewers) or []
621 reviewer_objects = Optional.extract(reviewers) or []
680 if reviewer_objects:
622 if reviewer_objects:
681 schema = ReviewerListSchema()
623 schema = ReviewerListSchema()
682 try:
624 try:
683 reviewer_objects = schema.deserialize(reviewer_objects)
625 reviewer_objects = schema.deserialize(reviewer_objects)
684 except Invalid as err:
626 except Invalid as err:
685 raise JSONRPCValidationError(colander_exc=err)
627 raise JSONRPCValidationError(colander_exc=err)
686
628
687 reviewers = []
629 reviewers = []
688 for reviewer_object in reviewer_objects:
630 for reviewer_object in reviewer_objects:
689 user = get_user_or_error(reviewer_object['username'])
631 user = get_user_or_error(reviewer_object['username'])
690 reasons = reviewer_object['reasons']
632 reasons = reviewer_object['reasons']
691 mandatory = reviewer_object['mandatory']
633 mandatory = reviewer_object['mandatory']
692 reviewers.append((user.user_id, reasons, mandatory))
634 reviewers.append((user.user_id, reasons, mandatory))
693
635
694 title = Optional.extract(title)
636 title = Optional.extract(title)
695 description = Optional.extract(description)
637 description = Optional.extract(description)
696 if title or description:
638 if title or description:
697 PullRequestModel().edit(
639 PullRequestModel().edit(
698 pull_request, title or pull_request.title,
640 pull_request, title or pull_request.title,
699 description or pull_request.description)
641 description or pull_request.description)
700 Session().commit()
642 Session().commit()
701
643
702 commit_changes = {"added": [], "common": [], "removed": []}
644 commit_changes = {"added": [], "common": [], "removed": []}
703 if str2bool(Optional.extract(update_commits)):
645 if str2bool(Optional.extract(update_commits)):
704 if PullRequestModel().has_valid_update_type(pull_request):
646 if PullRequestModel().has_valid_update_type(pull_request):
705 update_response = PullRequestModel().update_commits(
647 update_response = PullRequestModel().update_commits(
706 pull_request)
648 pull_request)
707 commit_changes = update_response.changes or commit_changes
649 commit_changes = update_response.changes or commit_changes
708 Session().commit()
650 Session().commit()
709
651
710 reviewers_changes = {"added": [], "removed": []}
652 reviewers_changes = {"added": [], "removed": []}
711 if reviewers:
653 if reviewers:
712 added_reviewers, removed_reviewers = \
654 added_reviewers, removed_reviewers = \
713 PullRequestModel().update_reviewers(pull_request, reviewers)
655 PullRequestModel().update_reviewers(pull_request, reviewers)
714
656
715 reviewers_changes['added'] = sorted(
657 reviewers_changes['added'] = sorted(
716 [get_user_or_error(n).username for n in added_reviewers])
658 [get_user_or_error(n).username for n in added_reviewers])
717 reviewers_changes['removed'] = sorted(
659 reviewers_changes['removed'] = sorted(
718 [get_user_or_error(n).username for n in removed_reviewers])
660 [get_user_or_error(n).username for n in removed_reviewers])
719 Session().commit()
661 Session().commit()
720
662
721 if str2bool(Optional.extract(close_pull_request)):
722 PullRequestModel().close_pull_request_with_comment(
723 pull_request, apiuser, repo)
724 Session().commit()
725
726 data = {
663 data = {
727 'msg': 'Updated pull request `{}`'.format(
664 'msg': 'Updated pull request `{}`'.format(
728 pull_request.pull_request_id),
665 pull_request.pull_request_id),
729 'pull_request': pull_request.get_api_data(),
666 'pull_request': pull_request.get_api_data(),
730 'updated_commits': commit_changes,
667 'updated_commits': commit_changes,
731 'updated_reviewers': reviewers_changes
668 'updated_reviewers': reviewers_changes
732 }
669 }
733
670
734 return data
671 return data
672
673
674 @jsonrpc_method()
675 def close_pull_request(
676 request, apiuser, repoid, pullrequestid,
677 userid=Optional(OAttr('apiuser')), message=Optional('')):
678 """
679 Close the pull request specified by `pullrequestid`.
680
681 :param apiuser: This is filled automatically from the |authtoken|.
682 :type apiuser: AuthUser
683 :param repoid: Repository name or repository ID to which the pull
684 request belongs.
685 :type repoid: str or int
686 :param pullrequestid: ID of the pull request to be closed.
687 :type pullrequestid: int
688 :param userid: Close the pull request as this user.
689 :type userid: Optional(str or int)
690 :param message: Optional message to close the Pull Request with. If not
691 specified it will be generated automatically.
692 :type message: Optional(str)
693
694 Example output:
695
696 .. code-block:: bash
697
698 "id": <id_given_in_input>,
699 "result": {
700 "pull_request_id": "<int>",
701 "close_status": "<str:status_lbl>,
702 "closed": "<bool>"
703 },
704 "error": null
705
706 """
707 _ = request.translate
708
709 repo = get_repo_or_error(repoid)
710 if not isinstance(userid, Optional):
711 if (has_superadmin_permission(apiuser) or
712 HasRepoPermissionAnyApi('repository.admin')(
713 user=apiuser, repo_name=repo.repo_name)):
714 apiuser = get_user_or_error(userid)
715 else:
716 raise JSONRPCError('userid is not the same as your user')
717
718 pull_request = get_pull_request_or_error(pullrequestid)
719
720 if pull_request.is_closed():
721 raise JSONRPCError(
722 'pull request `%s` is already closed' % (pullrequestid,))
723
724 # only owner or admin or person with write permissions
725 allowed_to_close = PullRequestModel().check_user_update(
726 pull_request, apiuser, api=True)
727
728 if not allowed_to_close:
729 raise JSONRPCError(
730 'pull request `%s` close failed, no permission to close.' % (
731 pullrequestid,))
732
733 # message we're using to close the PR, else it's automatically generated
734 message = Optional.extract(message)
735
736 # finally close the PR, with proper message comment
737 comment, status = PullRequestModel().close_pull_request_with_comment(
738 pull_request, apiuser, repo, message=message)
739 status_lbl = ChangesetStatus.get_status_lbl(status)
740
741 Session().commit()
742
743 data = {
744 'pull_request_id': pull_request.pull_request_id,
745 'close_status': status_lbl,
746 'closed': True,
747 }
748 return data
@@ -1,486 +1,485 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 commit controller for RhodeCode showing changes between commits
22 commit controller for RhodeCode showing changes between commits
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from collections import defaultdict
27 from collections import defaultdict
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29
29
30 from pylons import tmpl_context as c, request, response
30 from pylons import tmpl_context as c, request, response
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33
33
34 from rhodecode.lib import auth
34 from rhodecode.lib import auth
35 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib import diffs, codeblocks
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 import rhodecode.lib.helpers as h
41 import rhodecode.lib.helpers as h
42 from rhodecode.lib.utils import action_logger, jsonify
42 from rhodecode.lib.utils import action_logger, jsonify
43 from rhodecode.lib.utils2 import safe_unicode
43 from rhodecode.lib.utils2 import safe_unicode
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, GET):
57 def _update_with_GET(params, GET):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += GET.getall(k)
59 params[k] += GET.getall(k)
60
60
61
61
62 def get_ignore_ws(fid, GET):
62 def get_ignore_ws(fid, GET):
63 ig_ws_global = GET.get('ignorews')
63 ig_ws_global = GET.get('ignorews')
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 if ig_ws:
65 if ig_ws:
66 try:
66 try:
67 return int(ig_ws[0].split(':')[-1])
67 return int(ig_ws[0].split(':')[-1])
68 except Exception:
68 except Exception:
69 pass
69 pass
70 return ig_ws_global
70 return ig_ws_global
71
71
72
72
73 def _ignorews_url(GET, fileid=None):
73 def _ignorews_url(GET, fileid=None):
74 fileid = str(fileid) if fileid else None
74 fileid = str(fileid) if fileid else None
75 params = defaultdict(list)
75 params = defaultdict(list)
76 _update_with_GET(params, GET)
76 _update_with_GET(params, GET)
77 label = _('Show whitespace')
77 label = _('Show whitespace')
78 tooltiplbl = _('Show whitespace for all diffs')
78 tooltiplbl = _('Show whitespace for all diffs')
79 ig_ws = get_ignore_ws(fileid, GET)
79 ig_ws = get_ignore_ws(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
81
81
82 if ig_ws is None:
82 if ig_ws is None:
83 params['ignorews'] += [1]
83 params['ignorews'] += [1]
84 label = _('Ignore whitespace')
84 label = _('Ignore whitespace')
85 tooltiplbl = _('Ignore whitespace for all diffs')
85 tooltiplbl = _('Ignore whitespace for all diffs')
86 ctx_key = 'context'
86 ctx_key = 'context'
87 ctx_val = ln_ctx
87 ctx_val = ln_ctx
88
88
89 # if we have passed in ln_ctx pass it along to our params
89 # if we have passed in ln_ctx pass it along to our params
90 if ln_ctx:
90 if ln_ctx:
91 params[ctx_key] += [ctx_val]
91 params[ctx_key] += [ctx_val]
92
92
93 if fileid:
93 if fileid:
94 params['anchor'] = 'a_' + fileid
94 params['anchor'] = 'a_' + fileid
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96
96
97
97
98 def get_line_ctx(fid, GET):
98 def get_line_ctx(fid, GET):
99 ln_ctx_global = GET.get('context')
99 ln_ctx_global = GET.get('context')
100 if fid:
100 if fid:
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 else:
102 else:
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 if ln_ctx:
105 if ln_ctx:
106 ln_ctx = [ln_ctx]
106 ln_ctx = [ln_ctx]
107
107
108 if ln_ctx:
108 if ln_ctx:
109 retval = ln_ctx[0].split(':')[-1]
109 retval = ln_ctx[0].split(':')[-1]
110 else:
110 else:
111 retval = ln_ctx_global
111 retval = ln_ctx_global
112
112
113 try:
113 try:
114 return int(retval)
114 return int(retval)
115 except Exception:
115 except Exception:
116 return 3
116 return 3
117
117
118
118
119 def _context_url(GET, fileid=None):
119 def _context_url(GET, fileid=None):
120 """
120 """
121 Generates a url for context lines.
121 Generates a url for context lines.
122
122
123 :param fileid:
123 :param fileid:
124 """
124 """
125
125
126 fileid = str(fileid) if fileid else None
126 fileid = str(fileid) if fileid else None
127 ig_ws = get_ignore_ws(fileid, GET)
127 ig_ws = get_ignore_ws(fileid, GET)
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129
129
130 params = defaultdict(list)
130 params = defaultdict(list)
131 _update_with_GET(params, GET)
131 _update_with_GET(params, GET)
132
132
133 if ln_ctx > 0:
133 if ln_ctx > 0:
134 params['context'] += [ln_ctx]
134 params['context'] += [ln_ctx]
135
135
136 if ig_ws:
136 if ig_ws:
137 ig_ws_key = 'ignorews'
137 ig_ws_key = 'ignorews'
138 ig_ws_val = 1
138 ig_ws_val = 1
139 params[ig_ws_key] += [ig_ws_val]
139 params[ig_ws_key] += [ig_ws_val]
140
140
141 lbl = _('Increase context')
141 lbl = _('Increase context')
142 tooltiplbl = _('Increase context for all diffs')
142 tooltiplbl = _('Increase context for all diffs')
143
143
144 if fileid:
144 if fileid:
145 params['anchor'] = 'a_' + fileid
145 params['anchor'] = 'a_' + fileid
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147
147
148
148
149 class ChangesetController(BaseRepoController):
149 class ChangesetController(BaseRepoController):
150
150
151 def __before__(self):
151 def __before__(self):
152 super(ChangesetController, self).__before__()
152 super(ChangesetController, self).__before__()
153 c.affected_files_cut_off = 60
153 c.affected_files_cut_off = 60
154
154
155 def _index(self, commit_id_range, method):
155 def _index(self, commit_id_range, method):
156 c.ignorews_url = _ignorews_url
156 c.ignorews_url = _ignorews_url
157 c.context_url = _context_url
157 c.context_url = _context_url
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159
159
160 # fetch global flags of ignore ws or context lines
160 # fetch global flags of ignore ws or context lines
161 context_lcl = get_line_ctx('', request.GET)
161 context_lcl = get_line_ctx('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163
163
164 # diff_limit will cut off the whole diff if the limit is applied
164 # diff_limit will cut off the whole diff if the limit is applied
165 # otherwise it will just hide the big files from the front-end
165 # otherwise it will just hide the big files from the front-end
166 diff_limit = self.cut_off_limit_diff
166 diff_limit = self.cut_off_limit_diff
167 file_limit = self.cut_off_limit_file
167 file_limit = self.cut_off_limit_file
168
168
169 # get ranges of commit ids if preset
169 # get ranges of commit ids if preset
170 commit_range = commit_id_range.split('...')[:2]
170 commit_range = commit_id_range.split('...')[:2]
171
171
172 try:
172 try:
173 pre_load = ['affected_files', 'author', 'branch', 'date',
173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 'message', 'parents']
174 'message', 'parents']
175
175
176 if len(commit_range) == 2:
176 if len(commit_range) == 2:
177 commits = c.rhodecode_repo.get_commits(
177 commits = c.rhodecode_repo.get_commits(
178 start_id=commit_range[0], end_id=commit_range[1],
178 start_id=commit_range[0], end_id=commit_range[1],
179 pre_load=pre_load)
179 pre_load=pre_load)
180 commits = list(commits)
180 commits = list(commits)
181 else:
181 else:
182 commits = [c.rhodecode_repo.get_commit(
182 commits = [c.rhodecode_repo.get_commit(
183 commit_id=commit_id_range, pre_load=pre_load)]
183 commit_id=commit_id_range, pre_load=pre_load)]
184
184
185 c.commit_ranges = commits
185 c.commit_ranges = commits
186 if not c.commit_ranges:
186 if not c.commit_ranges:
187 raise RepositoryError(
187 raise RepositoryError(
188 'The commit range returned an empty result')
188 'The commit range returned an empty result')
189 except CommitDoesNotExistError:
189 except CommitDoesNotExistError:
190 msg = _('No such commit exists for this repository')
190 msg = _('No such commit exists for this repository')
191 h.flash(msg, category='error')
191 h.flash(msg, category='error')
192 raise HTTPNotFound()
192 raise HTTPNotFound()
193 except Exception:
193 except Exception:
194 log.exception("General failure")
194 log.exception("General failure")
195 raise HTTPNotFound()
195 raise HTTPNotFound()
196
196
197 c.changes = OrderedDict()
197 c.changes = OrderedDict()
198 c.lines_added = 0
198 c.lines_added = 0
199 c.lines_deleted = 0
199 c.lines_deleted = 0
200
200
201 # auto collapse if we have more than limit
201 # auto collapse if we have more than limit
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
204
204
205 c.commit_statuses = ChangesetStatus.STATUSES
205 c.commit_statuses = ChangesetStatus.STATUSES
206 c.inline_comments = []
206 c.inline_comments = []
207 c.files = []
207 c.files = []
208
208
209 c.statuses = []
209 c.statuses = []
210 c.comments = []
210 c.comments = []
211 c.unresolved_comments = []
211 c.unresolved_comments = []
212 if len(c.commit_ranges) == 1:
212 if len(c.commit_ranges) == 1:
213 commit = c.commit_ranges[0]
213 commit = c.commit_ranges[0]
214 c.comments = CommentsModel().get_comments(
214 c.comments = CommentsModel().get_comments(
215 c.rhodecode_db_repo.repo_id,
215 c.rhodecode_db_repo.repo_id,
216 revision=commit.raw_id)
216 revision=commit.raw_id)
217 c.statuses.append(ChangesetStatusModel().get_status(
217 c.statuses.append(ChangesetStatusModel().get_status(
218 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 c.rhodecode_db_repo.repo_id, commit.raw_id))
219 # comments from PR
219 # comments from PR
220 statuses = ChangesetStatusModel().get_statuses(
220 statuses = ChangesetStatusModel().get_statuses(
221 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 c.rhodecode_db_repo.repo_id, commit.raw_id,
222 with_revisions=True)
222 with_revisions=True)
223 prs = set(st.pull_request for st in statuses
223 prs = set(st.pull_request for st in statuses
224 if st.pull_request is not None)
224 if st.pull_request is not None)
225 # from associated statuses, check the pull requests, and
225 # from associated statuses, check the pull requests, and
226 # show comments from them
226 # show comments from them
227 for pr in prs:
227 for pr in prs:
228 c.comments.extend(pr.comments)
228 c.comments.extend(pr.comments)
229
229
230 c.unresolved_comments = CommentsModel()\
230 c.unresolved_comments = CommentsModel()\
231 .get_commit_unresolved_todos(commit.raw_id)
231 .get_commit_unresolved_todos(commit.raw_id)
232
232
233 # Iterate over ranges (default commit view is always one commit)
233 # Iterate over ranges (default commit view is always one commit)
234 for commit in c.commit_ranges:
234 for commit in c.commit_ranges:
235 c.changes[commit.raw_id] = []
235 c.changes[commit.raw_id] = []
236
236
237 commit2 = commit
237 commit2 = commit
238 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
238 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
239
239
240 _diff = c.rhodecode_repo.get_diff(
240 _diff = c.rhodecode_repo.get_diff(
241 commit1, commit2,
241 commit1, commit2,
242 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
242 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
243 diff_processor = diffs.DiffProcessor(
243 diff_processor = diffs.DiffProcessor(
244 _diff, format='newdiff', diff_limit=diff_limit,
244 _diff, format='newdiff', diff_limit=diff_limit,
245 file_limit=file_limit, show_full_diff=fulldiff)
245 file_limit=file_limit, show_full_diff=fulldiff)
246
246
247 commit_changes = OrderedDict()
247 commit_changes = OrderedDict()
248 if method == 'show':
248 if method == 'show':
249 _parsed = diff_processor.prepare()
249 _parsed = diff_processor.prepare()
250 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
250 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
251
251
252 _parsed = diff_processor.prepare()
252 _parsed = diff_processor.prepare()
253
253
254 def _node_getter(commit):
254 def _node_getter(commit):
255 def get_node(fname):
255 def get_node(fname):
256 try:
256 try:
257 return commit.get_node(fname)
257 return commit.get_node(fname)
258 except NodeDoesNotExistError:
258 except NodeDoesNotExistError:
259 return None
259 return None
260 return get_node
260 return get_node
261
261
262 inline_comments = CommentsModel().get_inline_comments(
262 inline_comments = CommentsModel().get_inline_comments(
263 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
263 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
264 c.inline_cnt = CommentsModel().get_inline_comments_count(
264 c.inline_cnt = CommentsModel().get_inline_comments_count(
265 inline_comments)
265 inline_comments)
266
266
267 diffset = codeblocks.DiffSet(
267 diffset = codeblocks.DiffSet(
268 repo_name=c.repo_name,
268 repo_name=c.repo_name,
269 source_node_getter=_node_getter(commit1),
269 source_node_getter=_node_getter(commit1),
270 target_node_getter=_node_getter(commit2),
270 target_node_getter=_node_getter(commit2),
271 comments=inline_comments
271 comments=inline_comments
272 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
272 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
273 c.changes[commit.raw_id] = diffset
273 c.changes[commit.raw_id] = diffset
274 else:
274 else:
275 # downloads/raw we only need RAW diff nothing else
275 # downloads/raw we only need RAW diff nothing else
276 diff = diff_processor.as_raw()
276 diff = diff_processor.as_raw()
277 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
277 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
278
278
279 # sort comments by how they were generated
279 # sort comments by how they were generated
280 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
280 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
281
281
282 if len(c.commit_ranges) == 1:
282 if len(c.commit_ranges) == 1:
283 c.commit = c.commit_ranges[0]
283 c.commit = c.commit_ranges[0]
284 c.parent_tmpl = ''.join(
284 c.parent_tmpl = ''.join(
285 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
285 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
286 if method == 'download':
286 if method == 'download':
287 response.content_type = 'text/plain'
287 response.content_type = 'text/plain'
288 response.content_disposition = (
288 response.content_disposition = (
289 'attachment; filename=%s.diff' % commit_id_range[:12])
289 'attachment; filename=%s.diff' % commit_id_range[:12])
290 return diff
290 return diff
291 elif method == 'patch':
291 elif method == 'patch':
292 response.content_type = 'text/plain'
292 response.content_type = 'text/plain'
293 c.diff = safe_unicode(diff)
293 c.diff = safe_unicode(diff)
294 return render('changeset/patch_changeset.mako')
294 return render('changeset/patch_changeset.mako')
295 elif method == 'raw':
295 elif method == 'raw':
296 response.content_type = 'text/plain'
296 response.content_type = 'text/plain'
297 return diff
297 return diff
298 elif method == 'show':
298 elif method == 'show':
299 if len(c.commit_ranges) == 1:
299 if len(c.commit_ranges) == 1:
300 return render('changeset/changeset.mako')
300 return render('changeset/changeset.mako')
301 else:
301 else:
302 c.ancestor = None
302 c.ancestor = None
303 c.target_repo = c.rhodecode_db_repo
303 c.target_repo = c.rhodecode_db_repo
304 return render('changeset/changeset_range.mako')
304 return render('changeset/changeset_range.mako')
305
305
306 @LoginRequired()
306 @LoginRequired()
307 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
308 'repository.admin')
308 'repository.admin')
309 def index(self, revision, method='show'):
309 def index(self, revision, method='show'):
310 return self._index(revision, method=method)
310 return self._index(revision, method=method)
311
311
312 @LoginRequired()
312 @LoginRequired()
313 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
314 'repository.admin')
314 'repository.admin')
315 def changeset_raw(self, revision):
315 def changeset_raw(self, revision):
316 return self._index(revision, method='raw')
316 return self._index(revision, method='raw')
317
317
318 @LoginRequired()
318 @LoginRequired()
319 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
320 'repository.admin')
320 'repository.admin')
321 def changeset_patch(self, revision):
321 def changeset_patch(self, revision):
322 return self._index(revision, method='patch')
322 return self._index(revision, method='patch')
323
323
324 @LoginRequired()
324 @LoginRequired()
325 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
325 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
326 'repository.admin')
326 'repository.admin')
327 def changeset_download(self, revision):
327 def changeset_download(self, revision):
328 return self._index(revision, method='download')
328 return self._index(revision, method='download')
329
329
330 @LoginRequired()
330 @LoginRequired()
331 @NotAnonymous()
331 @NotAnonymous()
332 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
332 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
333 'repository.admin')
333 'repository.admin')
334 @auth.CSRFRequired()
334 @auth.CSRFRequired()
335 @jsonify
335 @jsonify
336 def comment(self, repo_name, revision):
336 def comment(self, repo_name, revision):
337 commit_id = revision
337 commit_id = revision
338 status = request.POST.get('changeset_status', None)
338 status = request.POST.get('changeset_status', None)
339 text = request.POST.get('text')
339 text = request.POST.get('text')
340 comment_type = request.POST.get('comment_type')
340 comment_type = request.POST.get('comment_type')
341 resolves_comment_id = request.POST.get('resolves_comment_id', None)
341 resolves_comment_id = request.POST.get('resolves_comment_id', None)
342
342
343 if status:
343 if status:
344 text = text or (_('Status change %(transition_icon)s %(status)s')
344 text = text or (_('Status change %(transition_icon)s %(status)s')
345 % {'transition_icon': '>',
345 % {'transition_icon': '>',
346 'status': ChangesetStatus.get_status_lbl(status)})
346 'status': ChangesetStatus.get_status_lbl(status)})
347
347
348 multi_commit_ids = []
348 multi_commit_ids = []
349 for _commit_id in request.POST.get('commit_ids', '').split(','):
349 for _commit_id in request.POST.get('commit_ids', '').split(','):
350 if _commit_id not in ['', None, EmptyCommit.raw_id]:
350 if _commit_id not in ['', None, EmptyCommit.raw_id]:
351 if _commit_id not in multi_commit_ids:
351 if _commit_id not in multi_commit_ids:
352 multi_commit_ids.append(_commit_id)
352 multi_commit_ids.append(_commit_id)
353
353
354 commit_ids = multi_commit_ids or [commit_id]
354 commit_ids = multi_commit_ids or [commit_id]
355
355
356 comment = None
356 comment = None
357 for current_id in filter(None, commit_ids):
357 for current_id in filter(None, commit_ids):
358 c.co = comment = CommentsModel().create(
358 c.co = comment = CommentsModel().create(
359 text=text,
359 text=text,
360 repo=c.rhodecode_db_repo.repo_id,
360 repo=c.rhodecode_db_repo.repo_id,
361 user=c.rhodecode_user.user_id,
361 user=c.rhodecode_user.user_id,
362 commit_id=current_id,
362 commit_id=current_id,
363 f_path=request.POST.get('f_path'),
363 f_path=request.POST.get('f_path'),
364 line_no=request.POST.get('line'),
364 line_no=request.POST.get('line'),
365 status_change=(ChangesetStatus.get_status_lbl(status)
365 status_change=(ChangesetStatus.get_status_lbl(status)
366 if status else None),
366 if status else None),
367 status_change_type=status,
367 status_change_type=status,
368 comment_type=comment_type,
368 comment_type=comment_type,
369 resolves_comment_id=resolves_comment_id
369 resolves_comment_id=resolves_comment_id
370 )
370 )
371 c.inline_comment = True if comment.line_no else False
372
371
373 # get status if set !
372 # get status if set !
374 if status:
373 if status:
375 # if latest status was from pull request and it's closed
374 # if latest status was from pull request and it's closed
376 # disallow changing status !
375 # disallow changing status !
377 # dont_allow_on_closed_pull_request = True !
376 # dont_allow_on_closed_pull_request = True !
378
377
379 try:
378 try:
380 ChangesetStatusModel().set_status(
379 ChangesetStatusModel().set_status(
381 c.rhodecode_db_repo.repo_id,
380 c.rhodecode_db_repo.repo_id,
382 status,
381 status,
383 c.rhodecode_user.user_id,
382 c.rhodecode_user.user_id,
384 comment,
383 comment,
385 revision=current_id,
384 revision=current_id,
386 dont_allow_on_closed_pull_request=True
385 dont_allow_on_closed_pull_request=True
387 )
386 )
388 except StatusChangeOnClosedPullRequestError:
387 except StatusChangeOnClosedPullRequestError:
389 msg = _('Changing the status of a commit associated with '
388 msg = _('Changing the status of a commit associated with '
390 'a closed pull request is not allowed')
389 'a closed pull request is not allowed')
391 log.exception(msg)
390 log.exception(msg)
392 h.flash(msg, category='warning')
391 h.flash(msg, category='warning')
393 return redirect(h.url(
392 return redirect(h.url(
394 'changeset_home', repo_name=repo_name,
393 'changeset_home', repo_name=repo_name,
395 revision=current_id))
394 revision=current_id))
396
395
397 # finalize, commit and redirect
396 # finalize, commit and redirect
398 Session().commit()
397 Session().commit()
399
398
400 data = {
399 data = {
401 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
400 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
402 }
401 }
403 if comment:
402 if comment:
404 data.update(comment.get_dict())
403 data.update(comment.get_dict())
405 data.update({'rendered_text':
404 data.update({'rendered_text':
406 render('changeset/changeset_comment_block.mako')})
405 render('changeset/changeset_comment_block.mako')})
407
406
408 return data
407 return data
409
408
410 @LoginRequired()
409 @LoginRequired()
411 @NotAnonymous()
410 @NotAnonymous()
412 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
411 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
413 'repository.admin')
412 'repository.admin')
414 @auth.CSRFRequired()
413 @auth.CSRFRequired()
415 def preview_comment(self):
414 def preview_comment(self):
416 # Technically a CSRF token is not needed as no state changes with this
415 # Technically a CSRF token is not needed as no state changes with this
417 # call. However, as this is a POST is better to have it, so automated
416 # call. However, as this is a POST is better to have it, so automated
418 # tools don't flag it as potential CSRF.
417 # tools don't flag it as potential CSRF.
419 # Post is required because the payload could be bigger than the maximum
418 # Post is required because the payload could be bigger than the maximum
420 # allowed by GET.
419 # allowed by GET.
421 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
420 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
422 raise HTTPBadRequest()
421 raise HTTPBadRequest()
423 text = request.POST.get('text')
422 text = request.POST.get('text')
424 renderer = request.POST.get('renderer') or 'rst'
423 renderer = request.POST.get('renderer') or 'rst'
425 if text:
424 if text:
426 return h.render(text, renderer=renderer, mentions=True)
425 return h.render(text, renderer=renderer, mentions=True)
427 return ''
426 return ''
428
427
429 @LoginRequired()
428 @LoginRequired()
430 @NotAnonymous()
429 @NotAnonymous()
431 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
430 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
432 'repository.admin')
431 'repository.admin')
433 @auth.CSRFRequired()
432 @auth.CSRFRequired()
434 @jsonify
433 @jsonify
435 def delete_comment(self, repo_name, comment_id):
434 def delete_comment(self, repo_name, comment_id):
436 comment = ChangesetComment.get(comment_id)
435 comment = ChangesetComment.get(comment_id)
437 if not comment:
436 if not comment:
438 log.debug('Comment with id:%s not found, skipping', comment_id)
437 log.debug('Comment with id:%s not found, skipping', comment_id)
439 # comment already deleted in another call probably
438 # comment already deleted in another call probably
440 return True
439 return True
441
440
442 owner = (comment.author.user_id == c.rhodecode_user.user_id)
441 owner = (comment.author.user_id == c.rhodecode_user.user_id)
443 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
442 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
444 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
443 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
445 CommentsModel().delete(comment=comment)
444 CommentsModel().delete(comment=comment)
446 Session().commit()
445 Session().commit()
447 return True
446 return True
448 else:
447 else:
449 raise HTTPForbidden()
448 raise HTTPForbidden()
450
449
451 @LoginRequired()
450 @LoginRequired()
452 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
451 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
453 'repository.admin')
452 'repository.admin')
454 @jsonify
453 @jsonify
455 def changeset_info(self, repo_name, revision):
454 def changeset_info(self, repo_name, revision):
456 if request.is_xhr:
455 if request.is_xhr:
457 try:
456 try:
458 return c.rhodecode_repo.get_commit(commit_id=revision)
457 return c.rhodecode_repo.get_commit(commit_id=revision)
459 except CommitDoesNotExistError as e:
458 except CommitDoesNotExistError as e:
460 return EmptyCommit(message=str(e))
459 return EmptyCommit(message=str(e))
461 else:
460 else:
462 raise HTTPBadRequest()
461 raise HTTPBadRequest()
463
462
464 @LoginRequired()
463 @LoginRequired()
465 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
464 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
466 'repository.admin')
465 'repository.admin')
467 @jsonify
466 @jsonify
468 def changeset_children(self, repo_name, revision):
467 def changeset_children(self, repo_name, revision):
469 if request.is_xhr:
468 if request.is_xhr:
470 commit = c.rhodecode_repo.get_commit(commit_id=revision)
469 commit = c.rhodecode_repo.get_commit(commit_id=revision)
471 result = {"results": commit.children}
470 result = {"results": commit.children}
472 return result
471 return result
473 else:
472 else:
474 raise HTTPBadRequest()
473 raise HTTPBadRequest()
475
474
476 @LoginRequired()
475 @LoginRequired()
477 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
476 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
478 'repository.admin')
477 'repository.admin')
479 @jsonify
478 @jsonify
480 def changeset_parents(self, repo_name, revision):
479 def changeset_parents(self, repo_name, revision):
481 if request.is_xhr:
480 if request.is_xhr:
482 commit = c.rhodecode_repo.get_commit(commit_id=revision)
481 commit = c.rhodecode_repo.get_commit(commit_id=revision)
483 result = {"results": commit.parents}
482 result = {"results": commit.parents}
484 return result
483 return result
485 else:
484 else:
486 raise HTTPBadRequest()
485 raise HTTPBadRequest()
@@ -1,1023 +1,1008 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69
69
70 def __before__(self):
70 def __before__(self):
71 super(PullrequestsController, self).__before__()
71 super(PullrequestsController, self).__before__()
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
74
74
75 @LoginRequired()
75 @LoginRequired()
76 @NotAnonymous()
76 @NotAnonymous()
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
78 'repository.admin')
78 'repository.admin')
79 @HasAcceptedRepoType('git', 'hg')
79 @HasAcceptedRepoType('git', 'hg')
80 def index(self):
80 def index(self):
81 source_repo = c.rhodecode_db_repo
81 source_repo = c.rhodecode_db_repo
82
82
83 try:
83 try:
84 source_repo.scm_instance().get_commit()
84 source_repo.scm_instance().get_commit()
85 except EmptyRepositoryError:
85 except EmptyRepositoryError:
86 h.flash(h.literal(_('There are no commits yet')),
86 h.flash(h.literal(_('There are no commits yet')),
87 category='warning')
87 category='warning')
88 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
88 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
89
89
90 commit_id = request.GET.get('commit')
90 commit_id = request.GET.get('commit')
91 branch_ref = request.GET.get('branch')
91 branch_ref = request.GET.get('branch')
92 bookmark_ref = request.GET.get('bookmark')
92 bookmark_ref = request.GET.get('bookmark')
93
93
94 try:
94 try:
95 source_repo_data = PullRequestModel().generate_repo_data(
95 source_repo_data = PullRequestModel().generate_repo_data(
96 source_repo, commit_id=commit_id,
96 source_repo, commit_id=commit_id,
97 branch=branch_ref, bookmark=bookmark_ref)
97 branch=branch_ref, bookmark=bookmark_ref)
98 except CommitDoesNotExistError as e:
98 except CommitDoesNotExistError as e:
99 log.exception(e)
99 log.exception(e)
100 h.flash(_('Commit does not exist'), 'error')
100 h.flash(_('Commit does not exist'), 'error')
101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
102
102
103 default_target_repo = source_repo
103 default_target_repo = source_repo
104
104
105 if source_repo.parent:
105 if source_repo.parent:
106 parent_vcs_obj = source_repo.parent.scm_instance()
106 parent_vcs_obj = source_repo.parent.scm_instance()
107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
108 # change default if we have a parent repo
108 # change default if we have a parent repo
109 default_target_repo = source_repo.parent
109 default_target_repo = source_repo.parent
110
110
111 target_repo_data = PullRequestModel().generate_repo_data(
111 target_repo_data = PullRequestModel().generate_repo_data(
112 default_target_repo)
112 default_target_repo)
113
113
114 selected_source_ref = source_repo_data['refs']['selected_ref']
114 selected_source_ref = source_repo_data['refs']['selected_ref']
115
115
116 title_source_ref = selected_source_ref.split(':', 2)[1]
116 title_source_ref = selected_source_ref.split(':', 2)[1]
117 c.default_title = PullRequestModel().generate_pullrequest_title(
117 c.default_title = PullRequestModel().generate_pullrequest_title(
118 source=source_repo.repo_name,
118 source=source_repo.repo_name,
119 source_ref=title_source_ref,
119 source_ref=title_source_ref,
120 target=default_target_repo.repo_name
120 target=default_target_repo.repo_name
121 )
121 )
122
122
123 c.default_repo_data = {
123 c.default_repo_data = {
124 'source_repo_name': source_repo.repo_name,
124 'source_repo_name': source_repo.repo_name,
125 'source_refs_json': json.dumps(source_repo_data),
125 'source_refs_json': json.dumps(source_repo_data),
126 'target_repo_name': default_target_repo.repo_name,
126 'target_repo_name': default_target_repo.repo_name,
127 'target_refs_json': json.dumps(target_repo_data),
127 'target_refs_json': json.dumps(target_repo_data),
128 }
128 }
129 c.default_source_ref = selected_source_ref
129 c.default_source_ref = selected_source_ref
130
130
131 return render('/pullrequests/pullrequest.mako')
131 return render('/pullrequests/pullrequest.mako')
132
132
133 @LoginRequired()
133 @LoginRequired()
134 @NotAnonymous()
134 @NotAnonymous()
135 @XHRRequired()
135 @XHRRequired()
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
137 'repository.admin')
137 'repository.admin')
138 @jsonify
138 @jsonify
139 def get_repo_refs(self, repo_name, target_repo_name):
139 def get_repo_refs(self, repo_name, target_repo_name):
140 repo = Repository.get_by_repo_name(target_repo_name)
140 repo = Repository.get_by_repo_name(target_repo_name)
141 if not repo:
141 if not repo:
142 raise HTTPNotFound
142 raise HTTPNotFound
143 return PullRequestModel().generate_repo_data(repo)
143 return PullRequestModel().generate_repo_data(repo)
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @NotAnonymous()
146 @NotAnonymous()
147 @XHRRequired()
147 @XHRRequired()
148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
149 'repository.admin')
149 'repository.admin')
150 @jsonify
150 @jsonify
151 def get_repo_destinations(self, repo_name):
151 def get_repo_destinations(self, repo_name):
152 repo = Repository.get_by_repo_name(repo_name)
152 repo = Repository.get_by_repo_name(repo_name)
153 if not repo:
153 if not repo:
154 raise HTTPNotFound
154 raise HTTPNotFound
155 filter_query = request.GET.get('query')
155 filter_query = request.GET.get('query')
156
156
157 query = Repository.query() \
157 query = Repository.query() \
158 .order_by(func.length(Repository.repo_name)) \
158 .order_by(func.length(Repository.repo_name)) \
159 .filter(or_(
159 .filter(or_(
160 Repository.repo_name == repo.repo_name,
160 Repository.repo_name == repo.repo_name,
161 Repository.fork_id == repo.repo_id))
161 Repository.fork_id == repo.repo_id))
162
162
163 if filter_query:
163 if filter_query:
164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
165 query = query.filter(
165 query = query.filter(
166 Repository.repo_name.ilike(ilike_expression))
166 Repository.repo_name.ilike(ilike_expression))
167
167
168 add_parent = False
168 add_parent = False
169 if repo.parent:
169 if repo.parent:
170 if filter_query in repo.parent.repo_name:
170 if filter_query in repo.parent.repo_name:
171 parent_vcs_obj = repo.parent.scm_instance()
171 parent_vcs_obj = repo.parent.scm_instance()
172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
173 add_parent = True
173 add_parent = True
174
174
175 limit = 20 - 1 if add_parent else 20
175 limit = 20 - 1 if add_parent else 20
176 all_repos = query.limit(limit).all()
176 all_repos = query.limit(limit).all()
177 if add_parent:
177 if add_parent:
178 all_repos += [repo.parent]
178 all_repos += [repo.parent]
179
179
180 repos = []
180 repos = []
181 for obj in self.scm_model.get_repos(all_repos):
181 for obj in self.scm_model.get_repos(all_repos):
182 repos.append({
182 repos.append({
183 'id': obj['name'],
183 'id': obj['name'],
184 'text': obj['name'],
184 'text': obj['name'],
185 'type': 'repo',
185 'type': 'repo',
186 'obj': obj['dbrepo']
186 'obj': obj['dbrepo']
187 })
187 })
188
188
189 data = {
189 data = {
190 'more': False,
190 'more': False,
191 'results': [{
191 'results': [{
192 'text': _('Repositories'),
192 'text': _('Repositories'),
193 'children': repos
193 'children': repos
194 }] if repos else []
194 }] if repos else []
195 }
195 }
196 return data
196 return data
197
197
198 @LoginRequired()
198 @LoginRequired()
199 @NotAnonymous()
199 @NotAnonymous()
200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
201 'repository.admin')
201 'repository.admin')
202 @HasAcceptedRepoType('git', 'hg')
202 @HasAcceptedRepoType('git', 'hg')
203 @auth.CSRFRequired()
203 @auth.CSRFRequired()
204 def create(self, repo_name):
204 def create(self, repo_name):
205 repo = Repository.get_by_repo_name(repo_name)
205 repo = Repository.get_by_repo_name(repo_name)
206 if not repo:
206 if not repo:
207 raise HTTPNotFound
207 raise HTTPNotFound
208
208
209 controls = peppercorn.parse(request.POST.items())
209 controls = peppercorn.parse(request.POST.items())
210
210
211 try:
211 try:
212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
213 except formencode.Invalid as errors:
213 except formencode.Invalid as errors:
214 if errors.error_dict.get('revisions'):
214 if errors.error_dict.get('revisions'):
215 msg = 'Revisions: %s' % errors.error_dict['revisions']
215 msg = 'Revisions: %s' % errors.error_dict['revisions']
216 elif errors.error_dict.get('pullrequest_title'):
216 elif errors.error_dict.get('pullrequest_title'):
217 msg = _('Pull request requires a title with min. 3 chars')
217 msg = _('Pull request requires a title with min. 3 chars')
218 else:
218 else:
219 msg = _('Error creating pull request: {}').format(errors)
219 msg = _('Error creating pull request: {}').format(errors)
220 log.exception(msg)
220 log.exception(msg)
221 h.flash(msg, 'error')
221 h.flash(msg, 'error')
222
222
223 # would rather just go back to form ...
223 # would rather just go back to form ...
224 return redirect(url('pullrequest_home', repo_name=repo_name))
224 return redirect(url('pullrequest_home', repo_name=repo_name))
225
225
226 source_repo = _form['source_repo']
226 source_repo = _form['source_repo']
227 source_ref = _form['source_ref']
227 source_ref = _form['source_ref']
228 target_repo = _form['target_repo']
228 target_repo = _form['target_repo']
229 target_ref = _form['target_ref']
229 target_ref = _form['target_ref']
230 commit_ids = _form['revisions'][::-1]
230 commit_ids = _form['revisions'][::-1]
231
231
232 # find the ancestor for this pr
232 # find the ancestor for this pr
233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
234 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
234 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
235
235
236 source_scm = source_db_repo.scm_instance()
236 source_scm = source_db_repo.scm_instance()
237 target_scm = target_db_repo.scm_instance()
237 target_scm = target_db_repo.scm_instance()
238
238
239 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
239 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
240 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
240 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
241
241
242 ancestor = source_scm.get_common_ancestor(
242 ancestor = source_scm.get_common_ancestor(
243 source_commit.raw_id, target_commit.raw_id, target_scm)
243 source_commit.raw_id, target_commit.raw_id, target_scm)
244
244
245 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
245 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
246 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
246 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
247
247
248 pullrequest_title = _form['pullrequest_title']
248 pullrequest_title = _form['pullrequest_title']
249 title_source_ref = source_ref.split(':', 2)[1]
249 title_source_ref = source_ref.split(':', 2)[1]
250 if not pullrequest_title:
250 if not pullrequest_title:
251 pullrequest_title = PullRequestModel().generate_pullrequest_title(
251 pullrequest_title = PullRequestModel().generate_pullrequest_title(
252 source=source_repo,
252 source=source_repo,
253 source_ref=title_source_ref,
253 source_ref=title_source_ref,
254 target=target_repo
254 target=target_repo
255 )
255 )
256
256
257 description = _form['pullrequest_desc']
257 description = _form['pullrequest_desc']
258
258
259 get_default_reviewers_data, validate_default_reviewers = \
259 get_default_reviewers_data, validate_default_reviewers = \
260 PullRequestModel().get_reviewer_functions()
260 PullRequestModel().get_reviewer_functions()
261
261
262 # recalculate reviewers logic, to make sure we can validate this
262 # recalculate reviewers logic, to make sure we can validate this
263 reviewer_rules = get_default_reviewers_data(
263 reviewer_rules = get_default_reviewers_data(
264 c.rhodecode_user.get_instance(), source_db_repo,
264 c.rhodecode_user.get_instance(), source_db_repo,
265 source_commit, target_db_repo, target_commit)
265 source_commit, target_db_repo, target_commit)
266
266
267 reviewers = validate_default_reviewers(
267 reviewers = validate_default_reviewers(
268 _form['review_members'], reviewer_rules)
268 _form['review_members'], reviewer_rules)
269
269
270 try:
270 try:
271 pull_request = PullRequestModel().create(
271 pull_request = PullRequestModel().create(
272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
273 target_ref, commit_ids, reviewers, pullrequest_title,
273 target_ref, commit_ids, reviewers, pullrequest_title,
274 description, reviewer_rules
274 description, reviewer_rules
275 )
275 )
276 Session().commit()
276 Session().commit()
277 h.flash(_('Successfully opened new pull request'),
277 h.flash(_('Successfully opened new pull request'),
278 category='success')
278 category='success')
279 except Exception as e:
279 except Exception as e:
280 msg = _('Error occurred during creation of this pull request.')
280 msg = _('Error occurred during creation of this pull request.')
281 log.exception(msg)
281 log.exception(msg)
282 h.flash(msg, category='error')
282 h.flash(msg, category='error')
283 return redirect(url('pullrequest_home', repo_name=repo_name))
283 return redirect(url('pullrequest_home', repo_name=repo_name))
284
284
285 return redirect(url('pullrequest_show', repo_name=target_repo,
285 return redirect(url('pullrequest_show', repo_name=target_repo,
286 pull_request_id=pull_request.pull_request_id))
286 pull_request_id=pull_request.pull_request_id))
287
287
288 @LoginRequired()
288 @LoginRequired()
289 @NotAnonymous()
289 @NotAnonymous()
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
291 'repository.admin')
291 'repository.admin')
292 @auth.CSRFRequired()
292 @auth.CSRFRequired()
293 @jsonify
293 @jsonify
294 def update(self, repo_name, pull_request_id):
294 def update(self, repo_name, pull_request_id):
295 pull_request_id = safe_int(pull_request_id)
295 pull_request_id = safe_int(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
297 # only owner or admin can update it
297 # only owner or admin can update it
298 allowed_to_update = PullRequestModel().check_user_update(
298 allowed_to_update = PullRequestModel().check_user_update(
299 pull_request, c.rhodecode_user)
299 pull_request, c.rhodecode_user)
300 if allowed_to_update:
300 if allowed_to_update:
301 controls = peppercorn.parse(request.POST.items())
301 controls = peppercorn.parse(request.POST.items())
302
302
303 if 'review_members' in controls:
303 if 'review_members' in controls:
304 self._update_reviewers(
304 self._update_reviewers(
305 pull_request_id, controls['review_members'],
305 pull_request_id, controls['review_members'],
306 pull_request.reviewer_data)
306 pull_request.reviewer_data)
307 elif str2bool(request.POST.get('update_commits', 'false')):
307 elif str2bool(request.POST.get('update_commits', 'false')):
308 self._update_commits(pull_request)
308 self._update_commits(pull_request)
309 elif str2bool(request.POST.get('close_pull_request', 'false')):
310 self._reject_close(pull_request)
311 elif str2bool(request.POST.get('edit_pull_request', 'false')):
309 elif str2bool(request.POST.get('edit_pull_request', 'false')):
312 self._edit_pull_request(pull_request)
310 self._edit_pull_request(pull_request)
313 else:
311 else:
314 raise HTTPBadRequest()
312 raise HTTPBadRequest()
315 return True
313 return True
316 raise HTTPForbidden()
314 raise HTTPForbidden()
317
315
318 def _edit_pull_request(self, pull_request):
316 def _edit_pull_request(self, pull_request):
319 try:
317 try:
320 PullRequestModel().edit(
318 PullRequestModel().edit(
321 pull_request, request.POST.get('title'),
319 pull_request, request.POST.get('title'),
322 request.POST.get('description'))
320 request.POST.get('description'))
323 except ValueError:
321 except ValueError:
324 msg = _(u'Cannot update closed pull requests.')
322 msg = _(u'Cannot update closed pull requests.')
325 h.flash(msg, category='error')
323 h.flash(msg, category='error')
326 return
324 return
327 else:
325 else:
328 Session().commit()
326 Session().commit()
329
327
330 msg = _(u'Pull request title & description updated.')
328 msg = _(u'Pull request title & description updated.')
331 h.flash(msg, category='success')
329 h.flash(msg, category='success')
332 return
330 return
333
331
334 def _update_commits(self, pull_request):
332 def _update_commits(self, pull_request):
335 resp = PullRequestModel().update_commits(pull_request)
333 resp = PullRequestModel().update_commits(pull_request)
336
334
337 if resp.executed:
335 if resp.executed:
338
336
339 if resp.target_changed and resp.source_changed:
337 if resp.target_changed and resp.source_changed:
340 changed = 'target and source repositories'
338 changed = 'target and source repositories'
341 elif resp.target_changed and not resp.source_changed:
339 elif resp.target_changed and not resp.source_changed:
342 changed = 'target repository'
340 changed = 'target repository'
343 elif not resp.target_changed and resp.source_changed:
341 elif not resp.target_changed and resp.source_changed:
344 changed = 'source repository'
342 changed = 'source repository'
345 else:
343 else:
346 changed = 'nothing'
344 changed = 'nothing'
347
345
348 msg = _(
346 msg = _(
349 u'Pull request updated to "{source_commit_id}" with '
347 u'Pull request updated to "{source_commit_id}" with '
350 u'{count_added} added, {count_removed} removed commits. '
348 u'{count_added} added, {count_removed} removed commits. '
351 u'Source of changes: {change_source}')
349 u'Source of changes: {change_source}')
352 msg = msg.format(
350 msg = msg.format(
353 source_commit_id=pull_request.source_ref_parts.commit_id,
351 source_commit_id=pull_request.source_ref_parts.commit_id,
354 count_added=len(resp.changes.added),
352 count_added=len(resp.changes.added),
355 count_removed=len(resp.changes.removed),
353 count_removed=len(resp.changes.removed),
356 change_source=changed)
354 change_source=changed)
357 h.flash(msg, category='success')
355 h.flash(msg, category='success')
358
356
359 registry = get_current_registry()
357 registry = get_current_registry()
360 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
361 channelstream_config = rhodecode_plugins.get('channelstream', {})
359 channelstream_config = rhodecode_plugins.get('channelstream', {})
362 if channelstream_config.get('enabled'):
360 if channelstream_config.get('enabled'):
363 message = msg + (
361 message = msg + (
364 ' - <a onclick="window.location.reload()">'
362 ' - <a onclick="window.location.reload()">'
365 '<strong>{}</strong></a>'.format(_('Reload page')))
363 '<strong>{}</strong></a>'.format(_('Reload page')))
366 channel = '/repo${}$/pr/{}'.format(
364 channel = '/repo${}$/pr/{}'.format(
367 pull_request.target_repo.repo_name,
365 pull_request.target_repo.repo_name,
368 pull_request.pull_request_id
366 pull_request.pull_request_id
369 )
367 )
370 payload = {
368 payload = {
371 'type': 'message',
369 'type': 'message',
372 'user': 'system',
370 'user': 'system',
373 'exclude_users': [request.user.username],
371 'exclude_users': [request.user.username],
374 'channel': channel,
372 'channel': channel,
375 'message': {
373 'message': {
376 'message': message,
374 'message': message,
377 'level': 'success',
375 'level': 'success',
378 'topic': '/notifications'
376 'topic': '/notifications'
379 }
377 }
380 }
378 }
381 channelstream_request(
379 channelstream_request(
382 channelstream_config, [payload], '/message',
380 channelstream_config, [payload], '/message',
383 raise_exc=False)
381 raise_exc=False)
384 else:
382 else:
385 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
386 warning_reasons = [
384 warning_reasons = [
387 UpdateFailureReason.NO_CHANGE,
385 UpdateFailureReason.NO_CHANGE,
388 UpdateFailureReason.WRONG_REF_TYPE,
386 UpdateFailureReason.WRONG_REF_TYPE,
389 ]
387 ]
390 category = 'warning' if resp.reason in warning_reasons else 'error'
388 category = 'warning' if resp.reason in warning_reasons else 'error'
391 h.flash(msg, category=category)
389 h.flash(msg, category=category)
392
390
393 @auth.CSRFRequired()
391 @auth.CSRFRequired()
394 @LoginRequired()
392 @LoginRequired()
395 @NotAnonymous()
393 @NotAnonymous()
396 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
397 'repository.admin')
395 'repository.admin')
398 def merge(self, repo_name, pull_request_id):
396 def merge(self, repo_name, pull_request_id):
399 """
397 """
400 POST /{repo_name}/pull-request/{pull_request_id}
398 POST /{repo_name}/pull-request/{pull_request_id}
401
399
402 Merge will perform a server-side merge of the specified
400 Merge will perform a server-side merge of the specified
403 pull request, if the pull request is approved and mergeable.
401 pull request, if the pull request is approved and mergeable.
404 After successful merging, the pull request is automatically
402 After successful merging, the pull request is automatically
405 closed, with a relevant comment.
403 closed, with a relevant comment.
406 """
404 """
407 pull_request_id = safe_int(pull_request_id)
405 pull_request_id = safe_int(pull_request_id)
408 pull_request = PullRequest.get_or_404(pull_request_id)
406 pull_request = PullRequest.get_or_404(pull_request_id)
409 user = c.rhodecode_user
407 user = c.rhodecode_user
410
408
411 check = MergeCheck.validate(pull_request, user)
409 check = MergeCheck.validate(pull_request, user)
412 merge_possible = not check.failed
410 merge_possible = not check.failed
413
411
414 for err_type, error_msg in check.errors:
412 for err_type, error_msg in check.errors:
415 h.flash(error_msg, category=err_type)
413 h.flash(error_msg, category=err_type)
416
414
417 if merge_possible:
415 if merge_possible:
418 log.debug("Pre-conditions checked, trying to merge.")
416 log.debug("Pre-conditions checked, trying to merge.")
419 extras = vcs_operation_context(
417 extras = vcs_operation_context(
420 request.environ, repo_name=pull_request.target_repo.repo_name,
418 request.environ, repo_name=pull_request.target_repo.repo_name,
421 username=user.username, action='push',
419 username=user.username, action='push',
422 scm=pull_request.target_repo.repo_type)
420 scm=pull_request.target_repo.repo_type)
423 self._merge_pull_request(pull_request, user, extras)
421 self._merge_pull_request(pull_request, user, extras)
424
422
425 return redirect(url(
423 return redirect(url(
426 'pullrequest_show',
424 'pullrequest_show',
427 repo_name=pull_request.target_repo.repo_name,
425 repo_name=pull_request.target_repo.repo_name,
428 pull_request_id=pull_request.pull_request_id))
426 pull_request_id=pull_request.pull_request_id))
429
427
430 def _merge_pull_request(self, pull_request, user, extras):
428 def _merge_pull_request(self, pull_request, user, extras):
431 merge_resp = PullRequestModel().merge(
429 merge_resp = PullRequestModel().merge(
432 pull_request, user, extras=extras)
430 pull_request, user, extras=extras)
433
431
434 if merge_resp.executed:
432 if merge_resp.executed:
435 log.debug("The merge was successful, closing the pull request.")
433 log.debug("The merge was successful, closing the pull request.")
436 PullRequestModel().close_pull_request(
434 PullRequestModel().close_pull_request(
437 pull_request.pull_request_id, user)
435 pull_request.pull_request_id, user)
438 Session().commit()
436 Session().commit()
439 msg = _('Pull request was successfully merged and closed.')
437 msg = _('Pull request was successfully merged and closed.')
440 h.flash(msg, category='success')
438 h.flash(msg, category='success')
441 else:
439 else:
442 log.debug(
440 log.debug(
443 "The merge was not successful. Merge response: %s",
441 "The merge was not successful. Merge response: %s",
444 merge_resp)
442 merge_resp)
445 msg = PullRequestModel().merge_status_message(
443 msg = PullRequestModel().merge_status_message(
446 merge_resp.failure_reason)
444 merge_resp.failure_reason)
447 h.flash(msg, category='error')
445 h.flash(msg, category='error')
448
446
449 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
447 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
450
448
451 get_default_reviewers_data, validate_default_reviewers = \
449 get_default_reviewers_data, validate_default_reviewers = \
452 PullRequestModel().get_reviewer_functions()
450 PullRequestModel().get_reviewer_functions()
453
451
454 try:
452 try:
455 reviewers = validate_default_reviewers(review_members, reviewer_rules)
453 reviewers = validate_default_reviewers(review_members, reviewer_rules)
456 except ValueError as e:
454 except ValueError as e:
457 log.error('Reviewers Validation:{}'.format(e))
455 log.error('Reviewers Validation:{}'.format(e))
458 h.flash(e, category='error')
456 h.flash(e, category='error')
459 return
457 return
460
458
461 PullRequestModel().update_reviewers(pull_request_id, reviewers)
459 PullRequestModel().update_reviewers(pull_request_id, reviewers)
462 h.flash(_('Pull request reviewers updated.'), category='success')
460 h.flash(_('Pull request reviewers updated.'), category='success')
463 Session().commit()
461 Session().commit()
464
462
465 def _reject_close(self, pull_request):
466 if pull_request.is_closed():
467 raise HTTPForbidden()
468
469 PullRequestModel().close_pull_request_with_comment(
470 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
471 Session().commit()
472
473 @LoginRequired()
463 @LoginRequired()
474 @NotAnonymous()
464 @NotAnonymous()
475 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
465 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
476 'repository.admin')
466 'repository.admin')
477 @auth.CSRFRequired()
467 @auth.CSRFRequired()
478 @jsonify
468 @jsonify
479 def delete(self, repo_name, pull_request_id):
469 def delete(self, repo_name, pull_request_id):
480 pull_request_id = safe_int(pull_request_id)
470 pull_request_id = safe_int(pull_request_id)
481 pull_request = PullRequest.get_or_404(pull_request_id)
471 pull_request = PullRequest.get_or_404(pull_request_id)
482
472
483 pr_closed = pull_request.is_closed()
473 pr_closed = pull_request.is_closed()
484 allowed_to_delete = PullRequestModel().check_user_delete(
474 allowed_to_delete = PullRequestModel().check_user_delete(
485 pull_request, c.rhodecode_user) and not pr_closed
475 pull_request, c.rhodecode_user) and not pr_closed
486
476
487 # only owner can delete it !
477 # only owner can delete it !
488 if allowed_to_delete:
478 if allowed_to_delete:
489 PullRequestModel().delete(pull_request)
479 PullRequestModel().delete(pull_request)
490 Session().commit()
480 Session().commit()
491 h.flash(_('Successfully deleted pull request'),
481 h.flash(_('Successfully deleted pull request'),
492 category='success')
482 category='success')
493 return redirect(url('my_account_pullrequests'))
483 return redirect(url('my_account_pullrequests'))
494
484
495 h.flash(_('Your are not allowed to delete this pull request'),
485 h.flash(_('Your are not allowed to delete this pull request'),
496 category='error')
486 category='error')
497 raise HTTPForbidden()
487 raise HTTPForbidden()
498
488
499 def _get_pr_version(self, pull_request_id, version=None):
489 def _get_pr_version(self, pull_request_id, version=None):
500 pull_request_id = safe_int(pull_request_id)
490 pull_request_id = safe_int(pull_request_id)
501 at_version = None
491 at_version = None
502
492
503 if version and version == 'latest':
493 if version and version == 'latest':
504 pull_request_ver = PullRequest.get(pull_request_id)
494 pull_request_ver = PullRequest.get(pull_request_id)
505 pull_request_obj = pull_request_ver
495 pull_request_obj = pull_request_ver
506 _org_pull_request_obj = pull_request_obj
496 _org_pull_request_obj = pull_request_obj
507 at_version = 'latest'
497 at_version = 'latest'
508 elif version:
498 elif version:
509 pull_request_ver = PullRequestVersion.get_or_404(version)
499 pull_request_ver = PullRequestVersion.get_or_404(version)
510 pull_request_obj = pull_request_ver
500 pull_request_obj = pull_request_ver
511 _org_pull_request_obj = pull_request_ver.pull_request
501 _org_pull_request_obj = pull_request_ver.pull_request
512 at_version = pull_request_ver.pull_request_version_id
502 at_version = pull_request_ver.pull_request_version_id
513 else:
503 else:
514 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
504 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
515 pull_request_id)
505 pull_request_id)
516
506
517 pull_request_display_obj = PullRequest.get_pr_display_object(
507 pull_request_display_obj = PullRequest.get_pr_display_object(
518 pull_request_obj, _org_pull_request_obj)
508 pull_request_obj, _org_pull_request_obj)
519
509
520 return _org_pull_request_obj, pull_request_obj, \
510 return _org_pull_request_obj, pull_request_obj, \
521 pull_request_display_obj, at_version
511 pull_request_display_obj, at_version
522
512
523 def _get_diffset(
513 def _get_diffset(
524 self, source_repo, source_ref_id, target_ref_id, target_commit,
514 self, source_repo, source_ref_id, target_ref_id, target_commit,
525 source_commit, diff_limit, file_limit, display_inline_comments):
515 source_commit, diff_limit, file_limit, display_inline_comments):
526 vcs_diff = PullRequestModel().get_diff(
516 vcs_diff = PullRequestModel().get_diff(
527 source_repo, source_ref_id, target_ref_id)
517 source_repo, source_ref_id, target_ref_id)
528
518
529 diff_processor = diffs.DiffProcessor(
519 diff_processor = diffs.DiffProcessor(
530 vcs_diff, format='newdiff', diff_limit=diff_limit,
520 vcs_diff, format='newdiff', diff_limit=diff_limit,
531 file_limit=file_limit, show_full_diff=c.fulldiff)
521 file_limit=file_limit, show_full_diff=c.fulldiff)
532
522
533 _parsed = diff_processor.prepare()
523 _parsed = diff_processor.prepare()
534
524
535 def _node_getter(commit):
525 def _node_getter(commit):
536 def get_node(fname):
526 def get_node(fname):
537 try:
527 try:
538 return commit.get_node(fname)
528 return commit.get_node(fname)
539 except NodeDoesNotExistError:
529 except NodeDoesNotExistError:
540 return None
530 return None
541
531
542 return get_node
532 return get_node
543
533
544 diffset = codeblocks.DiffSet(
534 diffset = codeblocks.DiffSet(
545 repo_name=c.repo_name,
535 repo_name=c.repo_name,
546 source_repo_name=c.source_repo.repo_name,
536 source_repo_name=c.source_repo.repo_name,
547 source_node_getter=_node_getter(target_commit),
537 source_node_getter=_node_getter(target_commit),
548 target_node_getter=_node_getter(source_commit),
538 target_node_getter=_node_getter(source_commit),
549 comments=display_inline_comments
539 comments=display_inline_comments
550 )
540 )
551 diffset = diffset.render_patchset(
541 diffset = diffset.render_patchset(
552 _parsed, target_commit.raw_id, source_commit.raw_id)
542 _parsed, target_commit.raw_id, source_commit.raw_id)
553
543
554 return diffset
544 return diffset
555
545
556 @LoginRequired()
546 @LoginRequired()
557 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
547 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
558 'repository.admin')
548 'repository.admin')
559 def show(self, repo_name, pull_request_id):
549 def show(self, repo_name, pull_request_id):
560 pull_request_id = safe_int(pull_request_id)
550 pull_request_id = safe_int(pull_request_id)
561 version = request.GET.get('version')
551 version = request.GET.get('version')
562 from_version = request.GET.get('from_version') or version
552 from_version = request.GET.get('from_version') or version
563 merge_checks = request.GET.get('merge_checks')
553 merge_checks = request.GET.get('merge_checks')
564 c.fulldiff = str2bool(request.GET.get('fulldiff'))
554 c.fulldiff = str2bool(request.GET.get('fulldiff'))
565
555
566 (pull_request_latest,
556 (pull_request_latest,
567 pull_request_at_ver,
557 pull_request_at_ver,
568 pull_request_display_obj,
558 pull_request_display_obj,
569 at_version) = self._get_pr_version(
559 at_version) = self._get_pr_version(
570 pull_request_id, version=version)
560 pull_request_id, version=version)
571 pr_closed = pull_request_latest.is_closed()
561 pr_closed = pull_request_latest.is_closed()
572
562
573 if pr_closed and (version or from_version):
563 if pr_closed and (version or from_version):
574 # not allow to browse versions
564 # not allow to browse versions
575 return redirect(h.url('pullrequest_show', repo_name=repo_name,
565 return redirect(h.url('pullrequest_show', repo_name=repo_name,
576 pull_request_id=pull_request_id))
566 pull_request_id=pull_request_id))
577
567
578 versions = pull_request_display_obj.versions()
568 versions = pull_request_display_obj.versions()
579
569
580 c.at_version = at_version
570 c.at_version = at_version
581 c.at_version_num = (at_version
571 c.at_version_num = (at_version
582 if at_version and at_version != 'latest'
572 if at_version and at_version != 'latest'
583 else None)
573 else None)
584 c.at_version_pos = ChangesetComment.get_index_from_version(
574 c.at_version_pos = ChangesetComment.get_index_from_version(
585 c.at_version_num, versions)
575 c.at_version_num, versions)
586
576
587 (prev_pull_request_latest,
577 (prev_pull_request_latest,
588 prev_pull_request_at_ver,
578 prev_pull_request_at_ver,
589 prev_pull_request_display_obj,
579 prev_pull_request_display_obj,
590 prev_at_version) = self._get_pr_version(
580 prev_at_version) = self._get_pr_version(
591 pull_request_id, version=from_version)
581 pull_request_id, version=from_version)
592
582
593 c.from_version = prev_at_version
583 c.from_version = prev_at_version
594 c.from_version_num = (prev_at_version
584 c.from_version_num = (prev_at_version
595 if prev_at_version and prev_at_version != 'latest'
585 if prev_at_version and prev_at_version != 'latest'
596 else None)
586 else None)
597 c.from_version_pos = ChangesetComment.get_index_from_version(
587 c.from_version_pos = ChangesetComment.get_index_from_version(
598 c.from_version_num, versions)
588 c.from_version_num, versions)
599
589
600 # define if we're in COMPARE mode or VIEW at version mode
590 # define if we're in COMPARE mode or VIEW at version mode
601 compare = at_version != prev_at_version
591 compare = at_version != prev_at_version
602
592
603 # pull_requests repo_name we opened it against
593 # pull_requests repo_name we opened it against
604 # ie. target_repo must match
594 # ie. target_repo must match
605 if repo_name != pull_request_at_ver.target_repo.repo_name:
595 if repo_name != pull_request_at_ver.target_repo.repo_name:
606 raise HTTPNotFound
596 raise HTTPNotFound
607
597
608 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
598 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
609 pull_request_at_ver)
599 pull_request_at_ver)
610
600
611 c.pull_request = pull_request_display_obj
601 c.pull_request = pull_request_display_obj
612 c.pull_request_latest = pull_request_latest
602 c.pull_request_latest = pull_request_latest
613
603
614 if compare or (at_version and not at_version == 'latest'):
604 if compare or (at_version and not at_version == 'latest'):
615 c.allowed_to_change_status = False
605 c.allowed_to_change_status = False
616 c.allowed_to_update = False
606 c.allowed_to_update = False
617 c.allowed_to_merge = False
607 c.allowed_to_merge = False
618 c.allowed_to_delete = False
608 c.allowed_to_delete = False
619 c.allowed_to_comment = False
609 c.allowed_to_comment = False
620 c.allowed_to_close = False
610 c.allowed_to_close = False
621 else:
611 else:
622 can_change_status = PullRequestModel().check_user_change_status(
612 can_change_status = PullRequestModel().check_user_change_status(
623 pull_request_at_ver, c.rhodecode_user)
613 pull_request_at_ver, c.rhodecode_user)
624 c.allowed_to_change_status = can_change_status and not pr_closed
614 c.allowed_to_change_status = can_change_status and not pr_closed
625
615
626 c.allowed_to_update = PullRequestModel().check_user_update(
616 c.allowed_to_update = PullRequestModel().check_user_update(
627 pull_request_latest, c.rhodecode_user) and not pr_closed
617 pull_request_latest, c.rhodecode_user) and not pr_closed
628 c.allowed_to_merge = PullRequestModel().check_user_merge(
618 c.allowed_to_merge = PullRequestModel().check_user_merge(
629 pull_request_latest, c.rhodecode_user) and not pr_closed
619 pull_request_latest, c.rhodecode_user) and not pr_closed
630 c.allowed_to_delete = PullRequestModel().check_user_delete(
620 c.allowed_to_delete = PullRequestModel().check_user_delete(
631 pull_request_latest, c.rhodecode_user) and not pr_closed
621 pull_request_latest, c.rhodecode_user) and not pr_closed
632 c.allowed_to_comment = not pr_closed
622 c.allowed_to_comment = not pr_closed
633 c.allowed_to_close = c.allowed_to_merge and not pr_closed
623 c.allowed_to_close = c.allowed_to_merge and not pr_closed
634
624
635 c.forbid_adding_reviewers = False
625 c.forbid_adding_reviewers = False
636 c.forbid_author_to_review = False
626 c.forbid_author_to_review = False
637 c.forbid_commit_author_to_review = False
627 c.forbid_commit_author_to_review = False
638
628
639 if pull_request_latest.reviewer_data and \
629 if pull_request_latest.reviewer_data and \
640 'rules' in pull_request_latest.reviewer_data:
630 'rules' in pull_request_latest.reviewer_data:
641 rules = pull_request_latest.reviewer_data['rules'] or {}
631 rules = pull_request_latest.reviewer_data['rules'] or {}
642 try:
632 try:
643 c.forbid_adding_reviewers = rules.get(
633 c.forbid_adding_reviewers = rules.get(
644 'forbid_adding_reviewers')
634 'forbid_adding_reviewers')
645 c.forbid_author_to_review = rules.get(
635 c.forbid_author_to_review = rules.get(
646 'forbid_author_to_review')
636 'forbid_author_to_review')
647 c.forbid_commit_author_to_review = rules.get(
637 c.forbid_commit_author_to_review = rules.get(
648 'forbid_commit_author_to_review')
638 'forbid_commit_author_to_review')
649 except Exception:
639 except Exception:
650 pass
640 pass
651
641
652 # check merge capabilities
642 # check merge capabilities
653 _merge_check = MergeCheck.validate(
643 _merge_check = MergeCheck.validate(
654 pull_request_latest, user=c.rhodecode_user)
644 pull_request_latest, user=c.rhodecode_user)
655 c.pr_merge_errors = _merge_check.error_details
645 c.pr_merge_errors = _merge_check.error_details
656 c.pr_merge_possible = not _merge_check.failed
646 c.pr_merge_possible = not _merge_check.failed
657 c.pr_merge_message = _merge_check.merge_msg
647 c.pr_merge_message = _merge_check.merge_msg
658
648
659 c.pull_request_review_status = _merge_check.review_status
649 c.pull_request_review_status = _merge_check.review_status
660 if merge_checks:
650 if merge_checks:
661 return render('/pullrequests/pullrequest_merge_checks.mako')
651 return render('/pullrequests/pullrequest_merge_checks.mako')
662
652
663 comments_model = CommentsModel()
653 comments_model = CommentsModel()
664
654
665 # reviewers and statuses
655 # reviewers and statuses
666 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
656 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
667 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
657 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
668
658
669 # GENERAL COMMENTS with versions #
659 # GENERAL COMMENTS with versions #
670 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
660 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
671 q = q.order_by(ChangesetComment.comment_id.asc())
661 q = q.order_by(ChangesetComment.comment_id.asc())
672 general_comments = q
662 general_comments = q
673
663
674 # pick comments we want to render at current version
664 # pick comments we want to render at current version
675 c.comment_versions = comments_model.aggregate_comments(
665 c.comment_versions = comments_model.aggregate_comments(
676 general_comments, versions, c.at_version_num)
666 general_comments, versions, c.at_version_num)
677 c.comments = c.comment_versions[c.at_version_num]['until']
667 c.comments = c.comment_versions[c.at_version_num]['until']
678
668
679 # INLINE COMMENTS with versions #
669 # INLINE COMMENTS with versions #
680 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
670 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
681 q = q.order_by(ChangesetComment.comment_id.asc())
671 q = q.order_by(ChangesetComment.comment_id.asc())
682 inline_comments = q
672 inline_comments = q
683
673
684 c.inline_versions = comments_model.aggregate_comments(
674 c.inline_versions = comments_model.aggregate_comments(
685 inline_comments, versions, c.at_version_num, inline=True)
675 inline_comments, versions, c.at_version_num, inline=True)
686
676
687 # inject latest version
677 # inject latest version
688 latest_ver = PullRequest.get_pr_display_object(
678 latest_ver = PullRequest.get_pr_display_object(
689 pull_request_latest, pull_request_latest)
679 pull_request_latest, pull_request_latest)
690
680
691 c.versions = versions + [latest_ver]
681 c.versions = versions + [latest_ver]
692
682
693 # if we use version, then do not show later comments
683 # if we use version, then do not show later comments
694 # than current version
684 # than current version
695 display_inline_comments = collections.defaultdict(
685 display_inline_comments = collections.defaultdict(
696 lambda: collections.defaultdict(list))
686 lambda: collections.defaultdict(list))
697 for co in inline_comments:
687 for co in inline_comments:
698 if c.at_version_num:
688 if c.at_version_num:
699 # pick comments that are at least UPTO given version, so we
689 # pick comments that are at least UPTO given version, so we
700 # don't render comments for higher version
690 # don't render comments for higher version
701 should_render = co.pull_request_version_id and \
691 should_render = co.pull_request_version_id and \
702 co.pull_request_version_id <= c.at_version_num
692 co.pull_request_version_id <= c.at_version_num
703 else:
693 else:
704 # showing all, for 'latest'
694 # showing all, for 'latest'
705 should_render = True
695 should_render = True
706
696
707 if should_render:
697 if should_render:
708 display_inline_comments[co.f_path][co.line_no].append(co)
698 display_inline_comments[co.f_path][co.line_no].append(co)
709
699
710 # load diff data into template context, if we use compare mode then
700 # load diff data into template context, if we use compare mode then
711 # diff is calculated based on changes between versions of PR
701 # diff is calculated based on changes between versions of PR
712
702
713 source_repo = pull_request_at_ver.source_repo
703 source_repo = pull_request_at_ver.source_repo
714 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
704 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
715
705
716 target_repo = pull_request_at_ver.target_repo
706 target_repo = pull_request_at_ver.target_repo
717 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
707 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
718
708
719 if compare:
709 if compare:
720 # in compare switch the diff base to latest commit from prev version
710 # in compare switch the diff base to latest commit from prev version
721 target_ref_id = prev_pull_request_display_obj.revisions[0]
711 target_ref_id = prev_pull_request_display_obj.revisions[0]
722
712
723 # despite opening commits for bookmarks/branches/tags, we always
713 # despite opening commits for bookmarks/branches/tags, we always
724 # convert this to rev to prevent changes after bookmark or branch change
714 # convert this to rev to prevent changes after bookmark or branch change
725 c.source_ref_type = 'rev'
715 c.source_ref_type = 'rev'
726 c.source_ref = source_ref_id
716 c.source_ref = source_ref_id
727
717
728 c.target_ref_type = 'rev'
718 c.target_ref_type = 'rev'
729 c.target_ref = target_ref_id
719 c.target_ref = target_ref_id
730
720
731 c.source_repo = source_repo
721 c.source_repo = source_repo
732 c.target_repo = target_repo
722 c.target_repo = target_repo
733
723
734 # diff_limit is the old behavior, will cut off the whole diff
724 # diff_limit is the old behavior, will cut off the whole diff
735 # if the limit is applied otherwise will just hide the
725 # if the limit is applied otherwise will just hide the
736 # big files from the front-end
726 # big files from the front-end
737 diff_limit = self.cut_off_limit_diff
727 diff_limit = self.cut_off_limit_diff
738 file_limit = self.cut_off_limit_file
728 file_limit = self.cut_off_limit_file
739
729
740 c.commit_ranges = []
730 c.commit_ranges = []
741 source_commit = EmptyCommit()
731 source_commit = EmptyCommit()
742 target_commit = EmptyCommit()
732 target_commit = EmptyCommit()
743 c.missing_requirements = False
733 c.missing_requirements = False
744
734
745 source_scm = source_repo.scm_instance()
735 source_scm = source_repo.scm_instance()
746 target_scm = target_repo.scm_instance()
736 target_scm = target_repo.scm_instance()
747
737
748 # try first shadow repo, fallback to regular repo
738 # try first shadow repo, fallback to regular repo
749 try:
739 try:
750 commits_source_repo = pull_request_latest.get_shadow_repo()
740 commits_source_repo = pull_request_latest.get_shadow_repo()
751 except Exception:
741 except Exception:
752 log.debug('Failed to get shadow repo', exc_info=True)
742 log.debug('Failed to get shadow repo', exc_info=True)
753 commits_source_repo = source_scm
743 commits_source_repo = source_scm
754
744
755 c.commits_source_repo = commits_source_repo
745 c.commits_source_repo = commits_source_repo
756 commit_cache = {}
746 commit_cache = {}
757 try:
747 try:
758 pre_load = ["author", "branch", "date", "message"]
748 pre_load = ["author", "branch", "date", "message"]
759 show_revs = pull_request_at_ver.revisions
749 show_revs = pull_request_at_ver.revisions
760 for rev in show_revs:
750 for rev in show_revs:
761 comm = commits_source_repo.get_commit(
751 comm = commits_source_repo.get_commit(
762 commit_id=rev, pre_load=pre_load)
752 commit_id=rev, pre_load=pre_load)
763 c.commit_ranges.append(comm)
753 c.commit_ranges.append(comm)
764 commit_cache[comm.raw_id] = comm
754 commit_cache[comm.raw_id] = comm
765
755
766 # Order here matters, we first need to get target, and then
756 # Order here matters, we first need to get target, and then
767 # the source
757 # the source
768 target_commit = commits_source_repo.get_commit(
758 target_commit = commits_source_repo.get_commit(
769 commit_id=safe_str(target_ref_id))
759 commit_id=safe_str(target_ref_id))
770
760
771 source_commit = commits_source_repo.get_commit(
761 source_commit = commits_source_repo.get_commit(
772 commit_id=safe_str(source_ref_id))
762 commit_id=safe_str(source_ref_id))
773
763
774 except CommitDoesNotExistError:
764 except CommitDoesNotExistError:
775 log.warning(
765 log.warning(
776 'Failed to get commit from `{}` repo'.format(
766 'Failed to get commit from `{}` repo'.format(
777 commits_source_repo), exc_info=True)
767 commits_source_repo), exc_info=True)
778 except RepositoryRequirementError:
768 except RepositoryRequirementError:
779 log.warning(
769 log.warning(
780 'Failed to get all required data from repo', exc_info=True)
770 'Failed to get all required data from repo', exc_info=True)
781 c.missing_requirements = True
771 c.missing_requirements = True
782
772
783 c.ancestor = None # set it to None, to hide it from PR view
773 c.ancestor = None # set it to None, to hide it from PR view
784
774
785 try:
775 try:
786 ancestor_id = source_scm.get_common_ancestor(
776 ancestor_id = source_scm.get_common_ancestor(
787 source_commit.raw_id, target_commit.raw_id, target_scm)
777 source_commit.raw_id, target_commit.raw_id, target_scm)
788 c.ancestor_commit = source_scm.get_commit(ancestor_id)
778 c.ancestor_commit = source_scm.get_commit(ancestor_id)
789 except Exception:
779 except Exception:
790 c.ancestor_commit = None
780 c.ancestor_commit = None
791
781
792 c.statuses = source_repo.statuses(
782 c.statuses = source_repo.statuses(
793 [x.raw_id for x in c.commit_ranges])
783 [x.raw_id for x in c.commit_ranges])
794
784
795 # auto collapse if we have more than limit
785 # auto collapse if we have more than limit
796 collapse_limit = diffs.DiffProcessor._collapse_commits_over
786 collapse_limit = diffs.DiffProcessor._collapse_commits_over
797 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
787 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
798 c.compare_mode = compare
788 c.compare_mode = compare
799
789
800 c.missing_commits = False
790 c.missing_commits = False
801 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
791 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
802 or source_commit == target_commit):
792 or source_commit == target_commit):
803
793
804 c.missing_commits = True
794 c.missing_commits = True
805 else:
795 else:
806
796
807 c.diffset = self._get_diffset(
797 c.diffset = self._get_diffset(
808 commits_source_repo, source_ref_id, target_ref_id,
798 commits_source_repo, source_ref_id, target_ref_id,
809 target_commit, source_commit,
799 target_commit, source_commit,
810 diff_limit, file_limit, display_inline_comments)
800 diff_limit, file_limit, display_inline_comments)
811
801
812 c.limited_diff = c.diffset.limited_diff
802 c.limited_diff = c.diffset.limited_diff
813
803
814 # calculate removed files that are bound to comments
804 # calculate removed files that are bound to comments
815 comment_deleted_files = [
805 comment_deleted_files = [
816 fname for fname in display_inline_comments
806 fname for fname in display_inline_comments
817 if fname not in c.diffset.file_stats]
807 if fname not in c.diffset.file_stats]
818
808
819 c.deleted_files_comments = collections.defaultdict(dict)
809 c.deleted_files_comments = collections.defaultdict(dict)
820 for fname, per_line_comments in display_inline_comments.items():
810 for fname, per_line_comments in display_inline_comments.items():
821 if fname in comment_deleted_files:
811 if fname in comment_deleted_files:
822 c.deleted_files_comments[fname]['stats'] = 0
812 c.deleted_files_comments[fname]['stats'] = 0
823 c.deleted_files_comments[fname]['comments'] = list()
813 c.deleted_files_comments[fname]['comments'] = list()
824 for lno, comments in per_line_comments.items():
814 for lno, comments in per_line_comments.items():
825 c.deleted_files_comments[fname]['comments'].extend(
815 c.deleted_files_comments[fname]['comments'].extend(
826 comments)
816 comments)
827
817
828 # this is a hack to properly display links, when creating PR, the
818 # this is a hack to properly display links, when creating PR, the
829 # compare view and others uses different notation, and
819 # compare view and others uses different notation, and
830 # compare_commits.mako renders links based on the target_repo.
820 # compare_commits.mako renders links based on the target_repo.
831 # We need to swap that here to generate it properly on the html side
821 # We need to swap that here to generate it properly on the html side
832 c.target_repo = c.source_repo
822 c.target_repo = c.source_repo
833
823
834 c.commit_statuses = ChangesetStatus.STATUSES
824 c.commit_statuses = ChangesetStatus.STATUSES
835
825
836 c.show_version_changes = not pr_closed
826 c.show_version_changes = not pr_closed
837 if c.show_version_changes:
827 if c.show_version_changes:
838 cur_obj = pull_request_at_ver
828 cur_obj = pull_request_at_ver
839 prev_obj = prev_pull_request_at_ver
829 prev_obj = prev_pull_request_at_ver
840
830
841 old_commit_ids = prev_obj.revisions
831 old_commit_ids = prev_obj.revisions
842 new_commit_ids = cur_obj.revisions
832 new_commit_ids = cur_obj.revisions
843 commit_changes = PullRequestModel()._calculate_commit_id_changes(
833 commit_changes = PullRequestModel()._calculate_commit_id_changes(
844 old_commit_ids, new_commit_ids)
834 old_commit_ids, new_commit_ids)
845 c.commit_changes_summary = commit_changes
835 c.commit_changes_summary = commit_changes
846
836
847 # calculate the diff for commits between versions
837 # calculate the diff for commits between versions
848 c.commit_changes = []
838 c.commit_changes = []
849 mark = lambda cs, fw: list(
839 mark = lambda cs, fw: list(
850 h.itertools.izip_longest([], cs, fillvalue=fw))
840 h.itertools.izip_longest([], cs, fillvalue=fw))
851 for c_type, raw_id in mark(commit_changes.added, 'a') \
841 for c_type, raw_id in mark(commit_changes.added, 'a') \
852 + mark(commit_changes.removed, 'r') \
842 + mark(commit_changes.removed, 'r') \
853 + mark(commit_changes.common, 'c'):
843 + mark(commit_changes.common, 'c'):
854
844
855 if raw_id in commit_cache:
845 if raw_id in commit_cache:
856 commit = commit_cache[raw_id]
846 commit = commit_cache[raw_id]
857 else:
847 else:
858 try:
848 try:
859 commit = commits_source_repo.get_commit(raw_id)
849 commit = commits_source_repo.get_commit(raw_id)
860 except CommitDoesNotExistError:
850 except CommitDoesNotExistError:
861 # in case we fail extracting still use "dummy" commit
851 # in case we fail extracting still use "dummy" commit
862 # for display in commit diff
852 # for display in commit diff
863 commit = h.AttributeDict(
853 commit = h.AttributeDict(
864 {'raw_id': raw_id,
854 {'raw_id': raw_id,
865 'message': 'EMPTY or MISSING COMMIT'})
855 'message': 'EMPTY or MISSING COMMIT'})
866 c.commit_changes.append([c_type, commit])
856 c.commit_changes.append([c_type, commit])
867
857
868 # current user review statuses for each version
858 # current user review statuses for each version
869 c.review_versions = {}
859 c.review_versions = {}
870 if c.rhodecode_user.user_id in allowed_reviewers:
860 if c.rhodecode_user.user_id in allowed_reviewers:
871 for co in general_comments:
861 for co in general_comments:
872 if co.author.user_id == c.rhodecode_user.user_id:
862 if co.author.user_id == c.rhodecode_user.user_id:
873 # each comment has a status change
863 # each comment has a status change
874 status = co.status_change
864 status = co.status_change
875 if status:
865 if status:
876 _ver_pr = status[0].comment.pull_request_version_id
866 _ver_pr = status[0].comment.pull_request_version_id
877 c.review_versions[_ver_pr] = status[0]
867 c.review_versions[_ver_pr] = status[0]
878
868
879 return render('/pullrequests/pullrequest_show.mako')
869 return render('/pullrequests/pullrequest_show.mako')
880
870
881 @LoginRequired()
871 @LoginRequired()
882 @NotAnonymous()
872 @NotAnonymous()
883 @HasRepoPermissionAnyDecorator(
873 @HasRepoPermissionAnyDecorator(
884 'repository.read', 'repository.write', 'repository.admin')
874 'repository.read', 'repository.write', 'repository.admin')
885 @auth.CSRFRequired()
875 @auth.CSRFRequired()
886 @jsonify
876 @jsonify
887 def comment(self, repo_name, pull_request_id):
877 def comment(self, repo_name, pull_request_id):
888 pull_request_id = safe_int(pull_request_id)
878 pull_request_id = safe_int(pull_request_id)
889 pull_request = PullRequest.get_or_404(pull_request_id)
879 pull_request = PullRequest.get_or_404(pull_request_id)
890 if pull_request.is_closed():
880 if pull_request.is_closed():
881 log.debug('comment: forbidden because pull request is closed')
891 raise HTTPForbidden()
882 raise HTTPForbidden()
892
883
893 status = request.POST.get('changeset_status', None)
884 status = request.POST.get('changeset_status', None)
894 text = request.POST.get('text')
885 text = request.POST.get('text')
895 comment_type = request.POST.get('comment_type')
886 comment_type = request.POST.get('comment_type')
896 resolves_comment_id = request.POST.get('resolves_comment_id', None)
887 resolves_comment_id = request.POST.get('resolves_comment_id', None)
897 close_pull_request = request.POST.get('close_pull_request')
888 close_pull_request = request.POST.get('close_pull_request')
898
889
899 close_pr = False
890 # the logic here should work like following, if we submit close
900 # only owner or admin or person with write permissions
891 # pr comment, use `close_pull_request_with_comment` function
901 allowed_to_close = PullRequestModel().check_user_update(
892 # else handle regular comment logic
902 pull_request, c.rhodecode_user)
893 user = c.rhodecode_user
894 repo = c.rhodecode_db_repo
903
895
904 if close_pull_request and allowed_to_close:
896 if close_pull_request:
905 close_pr = True
897 # only owner or admin or person with write permissions
906 pull_request_review_status = pull_request.calculated_review_status()
898 allowed_to_close = PullRequestModel().check_user_update(
907 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
899 pull_request, c.rhodecode_user)
908 # approved only if we have voting consent
900 if not allowed_to_close:
909 status = ChangesetStatus.STATUS_APPROVED
901 log.debug('comment: forbidden because not allowed to close '
910 else:
902 'pull request %s', pull_request_id)
911 status = ChangesetStatus.STATUS_REJECTED
903 raise HTTPForbidden()
904 comment, status = PullRequestModel().close_pull_request_with_comment(
905 pull_request, user, repo, message=text)
906 Session().flush()
907 events.trigger(
908 events.PullRequestCommentEvent(pull_request, comment))
912
909
913 allowed_to_change_status = PullRequestModel().check_user_change_status(
910 else:
914 pull_request, c.rhodecode_user)
911 # regular comment case, could be inline, or one with status.
912 # for that one we check also permissions
913
914 allowed_to_change_status = PullRequestModel().check_user_change_status(
915 pull_request, c.rhodecode_user)
916
917 if status and allowed_to_change_status:
918 message = (_('Status change %(transition_icon)s %(status)s')
919 % {'transition_icon': '>',
920 'status': ChangesetStatus.get_status_lbl(status)})
921 text = text or message
915
922
916 if status and allowed_to_change_status:
923 comment = CommentsModel().create(
917 message = (_('Status change %(transition_icon)s %(status)s')
924 text=text,
918 % {'transition_icon': '>',
925 repo=c.rhodecode_db_repo.repo_id,
919 'status': ChangesetStatus.get_status_lbl(status)})
926 user=c.rhodecode_user.user_id,
920 if close_pr:
927 pull_request=pull_request_id,
921 message = _('Closing with') + ' ' + message
928 f_path=request.POST.get('f_path'),
922 text = text or message
929 line_no=request.POST.get('line'),
923 comm = CommentsModel().create(
930 status_change=(ChangesetStatus.get_status_lbl(status)
924 text=text,
931 if status and allowed_to_change_status else None),
925 repo=c.rhodecode_db_repo.repo_id,
932 status_change_type=(status
926 user=c.rhodecode_user.user_id,
933 if status and allowed_to_change_status else None),
927 pull_request=pull_request_id,
934 comment_type=comment_type,
928 f_path=request.POST.get('f_path'),
935 resolves_comment_id=resolves_comment_id
929 line_no=request.POST.get('line'),
936 )
930 status_change=(ChangesetStatus.get_status_lbl(status)
937
931 if status and allowed_to_change_status else None),
938 if allowed_to_change_status:
932 status_change_type=(status
939 # calculate old status before we change it
933 if status and allowed_to_change_status else None),
940 old_calculated_status = pull_request.calculated_review_status()
934 closing_pr=close_pr,
935 comment_type=comment_type,
936 resolves_comment_id=resolves_comment_id
937 )
938
941
939 if allowed_to_change_status:
942 # get status if set !
940 old_calculated_status = pull_request.calculated_review_status()
943 if status:
941 # get status if set !
944 ChangesetStatusModel().set_status(
942 if status:
945 c.rhodecode_db_repo.repo_id,
943 ChangesetStatusModel().set_status(
946 status,
944 c.rhodecode_db_repo.repo_id,
947 c.rhodecode_user.user_id,
945 status,
948 comment,
946 c.rhodecode_user.user_id,
949 pull_request=pull_request_id
947 comm,
950 )
948 pull_request=pull_request_id
949 )
950
951
951 Session().flush()
952 Session().flush()
952 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
953 events.trigger(
953 # we now calculate the status of pull request, and based on that
954 events.PullRequestCommentEvent(pull_request, comment))
954 # calculation we set the commits status
955 calculated_status = pull_request.calculated_review_status()
956 if old_calculated_status != calculated_status:
957 PullRequestModel()._trigger_pull_request_hook(
958 pull_request, c.rhodecode_user, 'review_status_change')
959
960 calculated_status_lbl = ChangesetStatus.get_status_lbl(
961 calculated_status)
962
955
963 if close_pr:
956 # we now calculate the status of pull request, and based on that
964 status_completed = (
957 # calculation we set the commits status
965 calculated_status in [ChangesetStatus.STATUS_APPROVED,
958 calculated_status = pull_request.calculated_review_status()
966 ChangesetStatus.STATUS_REJECTED])
959 if old_calculated_status != calculated_status:
967 if close_pull_request or status_completed:
960 PullRequestModel()._trigger_pull_request_hook(
968 PullRequestModel().close_pull_request(
961 pull_request, c.rhodecode_user, 'review_status_change')
969 pull_request_id, c.rhodecode_user)
970 else:
971 h.flash(_('Closing pull request on other statuses than '
972 'rejected or approved is forbidden. '
973 'Calculated status from all reviewers '
974 'is currently: %s') % calculated_status_lbl,
975 category='warning')
976
962
977 Session().commit()
963 Session().commit()
978
964
979 if not request.is_xhr:
965 if not request.is_xhr:
980 return redirect(h.url('pullrequest_show', repo_name=repo_name,
966 return redirect(h.url('pullrequest_show', repo_name=repo_name,
981 pull_request_id=pull_request_id))
967 pull_request_id=pull_request_id))
982
968
983 data = {
969 data = {
984 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
970 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
985 }
971 }
986 if comm:
972 if comment:
987 c.co = comm
973 c.co = comment
988 c.inline_comment = True if comm.line_no else False
974 rendered_comment = render('changeset/changeset_comment_block.mako')
989 data.update(comm.get_dict())
975 data.update(comment.get_dict())
990 data.update({'rendered_text':
976 data.update({'rendered_text': rendered_comment})
991 render('changeset/changeset_comment_block.mako')})
992
977
993 return data
978 return data
994
979
995 @LoginRequired()
980 @LoginRequired()
996 @NotAnonymous()
981 @NotAnonymous()
997 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
982 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
998 'repository.admin')
983 'repository.admin')
999 @auth.CSRFRequired()
984 @auth.CSRFRequired()
1000 @jsonify
985 @jsonify
1001 def delete_comment(self, repo_name, comment_id):
986 def delete_comment(self, repo_name, comment_id):
1002 return self._delete_comment(comment_id)
987 return self._delete_comment(comment_id)
1003
988
1004 def _delete_comment(self, comment_id):
989 def _delete_comment(self, comment_id):
1005 comment_id = safe_int(comment_id)
990 comment_id = safe_int(comment_id)
1006 co = ChangesetComment.get_or_404(comment_id)
991 co = ChangesetComment.get_or_404(comment_id)
1007 if co.pull_request.is_closed():
992 if co.pull_request.is_closed():
1008 # don't allow deleting comments on closed pull request
993 # don't allow deleting comments on closed pull request
1009 raise HTTPForbidden()
994 raise HTTPForbidden()
1010
995
1011 is_owner = co.author.user_id == c.rhodecode_user.user_id
996 is_owner = co.author.user_id == c.rhodecode_user.user_id
1012 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
997 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1013 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
998 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1014 old_calculated_status = co.pull_request.calculated_review_status()
999 old_calculated_status = co.pull_request.calculated_review_status()
1015 CommentsModel().delete(comment=co)
1000 CommentsModel().delete(comment=co)
1016 Session().commit()
1001 Session().commit()
1017 calculated_status = co.pull_request.calculated_review_status()
1002 calculated_status = co.pull_request.calculated_review_status()
1018 if old_calculated_status != calculated_status:
1003 if old_calculated_status != calculated_status:
1019 PullRequestModel()._trigger_pull_request_hook(
1004 PullRequestModel()._trigger_pull_request_hook(
1020 co.pull_request, c.rhodecode_user, 'review_status_change')
1005 co.pull_request, c.rhodecode_user, 'review_status_change')
1021 return True
1006 return True
1022 else:
1007 else:
1023 raise HTTPForbidden()
1008 raise HTTPForbidden()
@@ -1,4026 +1,4030 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
43 from sqlalchemy.sql.expression import true
44 from beaker.cache import cache_region
44 from beaker.cache import cache_region
45 from zope.cachedescriptors.property import Lazy as LazyProperty
45 from zope.cachedescriptors.property import Lazy as LazyProperty
46
46
47 from pylons.i18n.translation import lazy_ugettext as _
47 from pylons.i18n.translation import lazy_ugettext as _
48 from pyramid.threadlocal import get_current_request
48 from pyramid.threadlocal import get_current_request
49
49
50 from rhodecode.lib.vcs import get_vcs_instance
50 from rhodecode.lib.vcs import get_vcs_instance
51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 from rhodecode.lib.utils2 import (
52 from rhodecode.lib.utils2 import (
53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 glob2re, StrictAttributeDict, cleaned_uri)
55 glob2re, StrictAttributeDict, cleaned_uri)
56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.ext_json import json
57 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.caching_query import FromCache
58 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.encrypt import AESCipher
59 from rhodecode.lib.encrypt import AESCipher
60
60
61 from rhodecode.model.meta import Base, Session
61 from rhodecode.model.meta import Base, Session
62
62
63 URL_SEP = '/'
63 URL_SEP = '/'
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66 # =============================================================================
66 # =============================================================================
67 # BASE CLASSES
67 # BASE CLASSES
68 # =============================================================================
68 # =============================================================================
69
69
70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 # beaker.session.secret if first is not set.
71 # beaker.session.secret if first is not set.
72 # and initialized at environment.py
72 # and initialized at environment.py
73 ENCRYPTION_KEY = None
73 ENCRYPTION_KEY = None
74
74
75 # used to sort permissions by types, '#' used here is not allowed to be in
75 # used to sort permissions by types, '#' used here is not allowed to be in
76 # usernames, and it's very early in sorted string.printable table.
76 # usernames, and it's very early in sorted string.printable table.
77 PERMISSION_TYPE_SORT = {
77 PERMISSION_TYPE_SORT = {
78 'admin': '####',
78 'admin': '####',
79 'write': '###',
79 'write': '###',
80 'read': '##',
80 'read': '##',
81 'none': '#',
81 'none': '#',
82 }
82 }
83
83
84
84
85 def display_sort(obj):
85 def display_sort(obj):
86 """
86 """
87 Sort function used to sort permissions in .permissions() function of
87 Sort function used to sort permissions in .permissions() function of
88 Repository, RepoGroup, UserGroup. Also it put the default user in front
88 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 of all other resources
89 of all other resources
90 """
90 """
91
91
92 if obj.username == User.DEFAULT_USER:
92 if obj.username == User.DEFAULT_USER:
93 return '#####'
93 return '#####'
94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 return prefix + obj.username
95 return prefix + obj.username
96
96
97
97
98 def _hash_key(k):
98 def _hash_key(k):
99 return md5_safe(k)
99 return md5_safe(k)
100
100
101
101
102 class EncryptedTextValue(TypeDecorator):
102 class EncryptedTextValue(TypeDecorator):
103 """
103 """
104 Special column for encrypted long text data, use like::
104 Special column for encrypted long text data, use like::
105
105
106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107
107
108 This column is intelligent so if value is in unencrypted form it return
108 This column is intelligent so if value is in unencrypted form it return
109 unencrypted form, but on save it always encrypts
109 unencrypted form, but on save it always encrypts
110 """
110 """
111 impl = Text
111 impl = Text
112
112
113 def process_bind_param(self, value, dialect):
113 def process_bind_param(self, value, dialect):
114 if not value:
114 if not value:
115 return value
115 return value
116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 # protect against double encrypting if someone manually starts
117 # protect against double encrypting if someone manually starts
118 # doing
118 # doing
119 raise ValueError('value needs to be in unencrypted format, ie. '
119 raise ValueError('value needs to be in unencrypted format, ie. '
120 'not starting with enc$aes')
120 'not starting with enc$aes')
121 return 'enc$aes_hmac$%s' % AESCipher(
121 return 'enc$aes_hmac$%s' % AESCipher(
122 ENCRYPTION_KEY, hmac=True).encrypt(value)
122 ENCRYPTION_KEY, hmac=True).encrypt(value)
123
123
124 def process_result_value(self, value, dialect):
124 def process_result_value(self, value, dialect):
125 import rhodecode
125 import rhodecode
126
126
127 if not value:
127 if not value:
128 return value
128 return value
129
129
130 parts = value.split('$', 3)
130 parts = value.split('$', 3)
131 if not len(parts) == 3:
131 if not len(parts) == 3:
132 # probably not encrypted values
132 # probably not encrypted values
133 return value
133 return value
134 else:
134 else:
135 if parts[0] != 'enc':
135 if parts[0] != 'enc':
136 # parts ok but without our header ?
136 # parts ok but without our header ?
137 return value
137 return value
138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 'rhodecode.encrypted_values.strict') or True)
139 'rhodecode.encrypted_values.strict') or True)
140 # at that stage we know it's our encryption
140 # at that stage we know it's our encryption
141 if parts[1] == 'aes':
141 if parts[1] == 'aes':
142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 elif parts[1] == 'aes_hmac':
143 elif parts[1] == 'aes_hmac':
144 decrypted_data = AESCipher(
144 decrypted_data = AESCipher(
145 ENCRYPTION_KEY, hmac=True,
145 ENCRYPTION_KEY, hmac=True,
146 strict_verification=enc_strict_mode).decrypt(parts[2])
146 strict_verification=enc_strict_mode).decrypt(parts[2])
147 else:
147 else:
148 raise ValueError(
148 raise ValueError(
149 'Encryption type part is wrong, must be `aes` '
149 'Encryption type part is wrong, must be `aes` '
150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 return decrypted_data
151 return decrypted_data
152
152
153
153
154 class BaseModel(object):
154 class BaseModel(object):
155 """
155 """
156 Base Model for all classes
156 Base Model for all classes
157 """
157 """
158
158
159 @classmethod
159 @classmethod
160 def _get_keys(cls):
160 def _get_keys(cls):
161 """return column names for this model """
161 """return column names for this model """
162 return class_mapper(cls).c.keys()
162 return class_mapper(cls).c.keys()
163
163
164 def get_dict(self):
164 def get_dict(self):
165 """
165 """
166 return dict with keys and values corresponding
166 return dict with keys and values corresponding
167 to this model data """
167 to this model data """
168
168
169 d = {}
169 d = {}
170 for k in self._get_keys():
170 for k in self._get_keys():
171 d[k] = getattr(self, k)
171 d[k] = getattr(self, k)
172
172
173 # also use __json__() if present to get additional fields
173 # also use __json__() if present to get additional fields
174 _json_attr = getattr(self, '__json__', None)
174 _json_attr = getattr(self, '__json__', None)
175 if _json_attr:
175 if _json_attr:
176 # update with attributes from __json__
176 # update with attributes from __json__
177 if callable(_json_attr):
177 if callable(_json_attr):
178 _json_attr = _json_attr()
178 _json_attr = _json_attr()
179 for k, val in _json_attr.iteritems():
179 for k, val in _json_attr.iteritems():
180 d[k] = val
180 d[k] = val
181 return d
181 return d
182
182
183 def get_appstruct(self):
183 def get_appstruct(self):
184 """return list with keys and values tuples corresponding
184 """return list with keys and values tuples corresponding
185 to this model data """
185 to this model data """
186
186
187 l = []
187 l = []
188 for k in self._get_keys():
188 for k in self._get_keys():
189 l.append((k, getattr(self, k),))
189 l.append((k, getattr(self, k),))
190 return l
190 return l
191
191
192 def populate_obj(self, populate_dict):
192 def populate_obj(self, populate_dict):
193 """populate model with data from given populate_dict"""
193 """populate model with data from given populate_dict"""
194
194
195 for k in self._get_keys():
195 for k in self._get_keys():
196 if k in populate_dict:
196 if k in populate_dict:
197 setattr(self, k, populate_dict[k])
197 setattr(self, k, populate_dict[k])
198
198
199 @classmethod
199 @classmethod
200 def query(cls):
200 def query(cls):
201 return Session().query(cls)
201 return Session().query(cls)
202
202
203 @classmethod
203 @classmethod
204 def get(cls, id_):
204 def get(cls, id_):
205 if id_:
205 if id_:
206 return cls.query().get(id_)
206 return cls.query().get(id_)
207
207
208 @classmethod
208 @classmethod
209 def get_or_404(cls, id_, pyramid_exc=False):
209 def get_or_404(cls, id_, pyramid_exc=False):
210 if pyramid_exc:
210 if pyramid_exc:
211 # NOTE(marcink): backward compat, once migration to pyramid
211 # NOTE(marcink): backward compat, once migration to pyramid
212 # this should only use pyramid exceptions
212 # this should only use pyramid exceptions
213 from pyramid.httpexceptions import HTTPNotFound
213 from pyramid.httpexceptions import HTTPNotFound
214 else:
214 else:
215 from webob.exc import HTTPNotFound
215 from webob.exc import HTTPNotFound
216
216
217 try:
217 try:
218 id_ = int(id_)
218 id_ = int(id_)
219 except (TypeError, ValueError):
219 except (TypeError, ValueError):
220 raise HTTPNotFound
220 raise HTTPNotFound
221
221
222 res = cls.query().get(id_)
222 res = cls.query().get(id_)
223 if not res:
223 if not res:
224 raise HTTPNotFound
224 raise HTTPNotFound
225 return res
225 return res
226
226
227 @classmethod
227 @classmethod
228 def getAll(cls):
228 def getAll(cls):
229 # deprecated and left for backward compatibility
229 # deprecated and left for backward compatibility
230 return cls.get_all()
230 return cls.get_all()
231
231
232 @classmethod
232 @classmethod
233 def get_all(cls):
233 def get_all(cls):
234 return cls.query().all()
234 return cls.query().all()
235
235
236 @classmethod
236 @classmethod
237 def delete(cls, id_):
237 def delete(cls, id_):
238 obj = cls.query().get(id_)
238 obj = cls.query().get(id_)
239 Session().delete(obj)
239 Session().delete(obj)
240
240
241 @classmethod
241 @classmethod
242 def identity_cache(cls, session, attr_name, value):
242 def identity_cache(cls, session, attr_name, value):
243 exist_in_session = []
243 exist_in_session = []
244 for (item_cls, pkey), instance in session.identity_map.items():
244 for (item_cls, pkey), instance in session.identity_map.items():
245 if cls == item_cls and getattr(instance, attr_name) == value:
245 if cls == item_cls and getattr(instance, attr_name) == value:
246 exist_in_session.append(instance)
246 exist_in_session.append(instance)
247 if exist_in_session:
247 if exist_in_session:
248 if len(exist_in_session) == 1:
248 if len(exist_in_session) == 1:
249 return exist_in_session[0]
249 return exist_in_session[0]
250 log.exception(
250 log.exception(
251 'multiple objects with attr %s and '
251 'multiple objects with attr %s and '
252 'value %s found with same name: %r',
252 'value %s found with same name: %r',
253 attr_name, value, exist_in_session)
253 attr_name, value, exist_in_session)
254
254
255 def __repr__(self):
255 def __repr__(self):
256 if hasattr(self, '__unicode__'):
256 if hasattr(self, '__unicode__'):
257 # python repr needs to return str
257 # python repr needs to return str
258 try:
258 try:
259 return safe_str(self.__unicode__())
259 return safe_str(self.__unicode__())
260 except UnicodeDecodeError:
260 except UnicodeDecodeError:
261 pass
261 pass
262 return '<DB:%s>' % (self.__class__.__name__)
262 return '<DB:%s>' % (self.__class__.__name__)
263
263
264
264
265 class RhodeCodeSetting(Base, BaseModel):
265 class RhodeCodeSetting(Base, BaseModel):
266 __tablename__ = 'rhodecode_settings'
266 __tablename__ = 'rhodecode_settings'
267 __table_args__ = (
267 __table_args__ = (
268 UniqueConstraint('app_settings_name'),
268 UniqueConstraint('app_settings_name'),
269 {'extend_existing': True, 'mysql_engine': 'InnoDB',
269 {'extend_existing': True, 'mysql_engine': 'InnoDB',
270 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
270 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
271 )
271 )
272
272
273 SETTINGS_TYPES = {
273 SETTINGS_TYPES = {
274 'str': safe_str,
274 'str': safe_str,
275 'int': safe_int,
275 'int': safe_int,
276 'unicode': safe_unicode,
276 'unicode': safe_unicode,
277 'bool': str2bool,
277 'bool': str2bool,
278 'list': functools.partial(aslist, sep=',')
278 'list': functools.partial(aslist, sep=',')
279 }
279 }
280 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
280 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
281 GLOBAL_CONF_KEY = 'app_settings'
281 GLOBAL_CONF_KEY = 'app_settings'
282
282
283 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
283 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
284 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
284 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
285 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
285 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
286 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
286 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
287
287
288 def __init__(self, key='', val='', type='unicode'):
288 def __init__(self, key='', val='', type='unicode'):
289 self.app_settings_name = key
289 self.app_settings_name = key
290 self.app_settings_type = type
290 self.app_settings_type = type
291 self.app_settings_value = val
291 self.app_settings_value = val
292
292
293 @validates('_app_settings_value')
293 @validates('_app_settings_value')
294 def validate_settings_value(self, key, val):
294 def validate_settings_value(self, key, val):
295 assert type(val) == unicode
295 assert type(val) == unicode
296 return val
296 return val
297
297
298 @hybrid_property
298 @hybrid_property
299 def app_settings_value(self):
299 def app_settings_value(self):
300 v = self._app_settings_value
300 v = self._app_settings_value
301 _type = self.app_settings_type
301 _type = self.app_settings_type
302 if _type:
302 if _type:
303 _type = self.app_settings_type.split('.')[0]
303 _type = self.app_settings_type.split('.')[0]
304 # decode the encrypted value
304 # decode the encrypted value
305 if 'encrypted' in self.app_settings_type:
305 if 'encrypted' in self.app_settings_type:
306 cipher = EncryptedTextValue()
306 cipher = EncryptedTextValue()
307 v = safe_unicode(cipher.process_result_value(v, None))
307 v = safe_unicode(cipher.process_result_value(v, None))
308
308
309 converter = self.SETTINGS_TYPES.get(_type) or \
309 converter = self.SETTINGS_TYPES.get(_type) or \
310 self.SETTINGS_TYPES['unicode']
310 self.SETTINGS_TYPES['unicode']
311 return converter(v)
311 return converter(v)
312
312
313 @app_settings_value.setter
313 @app_settings_value.setter
314 def app_settings_value(self, val):
314 def app_settings_value(self, val):
315 """
315 """
316 Setter that will always make sure we use unicode in app_settings_value
316 Setter that will always make sure we use unicode in app_settings_value
317
317
318 :param val:
318 :param val:
319 """
319 """
320 val = safe_unicode(val)
320 val = safe_unicode(val)
321 # encode the encrypted value
321 # encode the encrypted value
322 if 'encrypted' in self.app_settings_type:
322 if 'encrypted' in self.app_settings_type:
323 cipher = EncryptedTextValue()
323 cipher = EncryptedTextValue()
324 val = safe_unicode(cipher.process_bind_param(val, None))
324 val = safe_unicode(cipher.process_bind_param(val, None))
325 self._app_settings_value = val
325 self._app_settings_value = val
326
326
327 @hybrid_property
327 @hybrid_property
328 def app_settings_type(self):
328 def app_settings_type(self):
329 return self._app_settings_type
329 return self._app_settings_type
330
330
331 @app_settings_type.setter
331 @app_settings_type.setter
332 def app_settings_type(self, val):
332 def app_settings_type(self, val):
333 if val.split('.')[0] not in self.SETTINGS_TYPES:
333 if val.split('.')[0] not in self.SETTINGS_TYPES:
334 raise Exception('type must be one of %s got %s'
334 raise Exception('type must be one of %s got %s'
335 % (self.SETTINGS_TYPES.keys(), val))
335 % (self.SETTINGS_TYPES.keys(), val))
336 self._app_settings_type = val
336 self._app_settings_type = val
337
337
338 def __unicode__(self):
338 def __unicode__(self):
339 return u"<%s('%s:%s[%s]')>" % (
339 return u"<%s('%s:%s[%s]')>" % (
340 self.__class__.__name__,
340 self.__class__.__name__,
341 self.app_settings_name, self.app_settings_value,
341 self.app_settings_name, self.app_settings_value,
342 self.app_settings_type
342 self.app_settings_type
343 )
343 )
344
344
345
345
346 class RhodeCodeUi(Base, BaseModel):
346 class RhodeCodeUi(Base, BaseModel):
347 __tablename__ = 'rhodecode_ui'
347 __tablename__ = 'rhodecode_ui'
348 __table_args__ = (
348 __table_args__ = (
349 UniqueConstraint('ui_key'),
349 UniqueConstraint('ui_key'),
350 {'extend_existing': True, 'mysql_engine': 'InnoDB',
350 {'extend_existing': True, 'mysql_engine': 'InnoDB',
351 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
351 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
352 )
352 )
353
353
354 HOOK_REPO_SIZE = 'changegroup.repo_size'
354 HOOK_REPO_SIZE = 'changegroup.repo_size'
355 # HG
355 # HG
356 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
356 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
357 HOOK_PULL = 'outgoing.pull_logger'
357 HOOK_PULL = 'outgoing.pull_logger'
358 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
358 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
359 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
359 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
360 HOOK_PUSH = 'changegroup.push_logger'
360 HOOK_PUSH = 'changegroup.push_logger'
361 HOOK_PUSH_KEY = 'pushkey.key_push'
361 HOOK_PUSH_KEY = 'pushkey.key_push'
362
362
363 # TODO: johbo: Unify way how hooks are configured for git and hg,
363 # TODO: johbo: Unify way how hooks are configured for git and hg,
364 # git part is currently hardcoded.
364 # git part is currently hardcoded.
365
365
366 # SVN PATTERNS
366 # SVN PATTERNS
367 SVN_BRANCH_ID = 'vcs_svn_branch'
367 SVN_BRANCH_ID = 'vcs_svn_branch'
368 SVN_TAG_ID = 'vcs_svn_tag'
368 SVN_TAG_ID = 'vcs_svn_tag'
369
369
370 ui_id = Column(
370 ui_id = Column(
371 "ui_id", Integer(), nullable=False, unique=True, default=None,
371 "ui_id", Integer(), nullable=False, unique=True, default=None,
372 primary_key=True)
372 primary_key=True)
373 ui_section = Column(
373 ui_section = Column(
374 "ui_section", String(255), nullable=True, unique=None, default=None)
374 "ui_section", String(255), nullable=True, unique=None, default=None)
375 ui_key = Column(
375 ui_key = Column(
376 "ui_key", String(255), nullable=True, unique=None, default=None)
376 "ui_key", String(255), nullable=True, unique=None, default=None)
377 ui_value = Column(
377 ui_value = Column(
378 "ui_value", String(255), nullable=True, unique=None, default=None)
378 "ui_value", String(255), nullable=True, unique=None, default=None)
379 ui_active = Column(
379 ui_active = Column(
380 "ui_active", Boolean(), nullable=True, unique=None, default=True)
380 "ui_active", Boolean(), nullable=True, unique=None, default=True)
381
381
382 def __repr__(self):
382 def __repr__(self):
383 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
383 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
384 self.ui_key, self.ui_value)
384 self.ui_key, self.ui_value)
385
385
386
386
387 class RepoRhodeCodeSetting(Base, BaseModel):
387 class RepoRhodeCodeSetting(Base, BaseModel):
388 __tablename__ = 'repo_rhodecode_settings'
388 __tablename__ = 'repo_rhodecode_settings'
389 __table_args__ = (
389 __table_args__ = (
390 UniqueConstraint(
390 UniqueConstraint(
391 'app_settings_name', 'repository_id',
391 'app_settings_name', 'repository_id',
392 name='uq_repo_rhodecode_setting_name_repo_id'),
392 name='uq_repo_rhodecode_setting_name_repo_id'),
393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
395 )
395 )
396
396
397 repository_id = Column(
397 repository_id = Column(
398 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
398 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
399 nullable=False)
399 nullable=False)
400 app_settings_id = Column(
400 app_settings_id = Column(
401 "app_settings_id", Integer(), nullable=False, unique=True,
401 "app_settings_id", Integer(), nullable=False, unique=True,
402 default=None, primary_key=True)
402 default=None, primary_key=True)
403 app_settings_name = Column(
403 app_settings_name = Column(
404 "app_settings_name", String(255), nullable=True, unique=None,
404 "app_settings_name", String(255), nullable=True, unique=None,
405 default=None)
405 default=None)
406 _app_settings_value = Column(
406 _app_settings_value = Column(
407 "app_settings_value", String(4096), nullable=True, unique=None,
407 "app_settings_value", String(4096), nullable=True, unique=None,
408 default=None)
408 default=None)
409 _app_settings_type = Column(
409 _app_settings_type = Column(
410 "app_settings_type", String(255), nullable=True, unique=None,
410 "app_settings_type", String(255), nullable=True, unique=None,
411 default=None)
411 default=None)
412
412
413 repository = relationship('Repository')
413 repository = relationship('Repository')
414
414
415 def __init__(self, repository_id, key='', val='', type='unicode'):
415 def __init__(self, repository_id, key='', val='', type='unicode'):
416 self.repository_id = repository_id
416 self.repository_id = repository_id
417 self.app_settings_name = key
417 self.app_settings_name = key
418 self.app_settings_type = type
418 self.app_settings_type = type
419 self.app_settings_value = val
419 self.app_settings_value = val
420
420
421 @validates('_app_settings_value')
421 @validates('_app_settings_value')
422 def validate_settings_value(self, key, val):
422 def validate_settings_value(self, key, val):
423 assert type(val) == unicode
423 assert type(val) == unicode
424 return val
424 return val
425
425
426 @hybrid_property
426 @hybrid_property
427 def app_settings_value(self):
427 def app_settings_value(self):
428 v = self._app_settings_value
428 v = self._app_settings_value
429 type_ = self.app_settings_type
429 type_ = self.app_settings_type
430 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
430 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
431 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
431 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
432 return converter(v)
432 return converter(v)
433
433
434 @app_settings_value.setter
434 @app_settings_value.setter
435 def app_settings_value(self, val):
435 def app_settings_value(self, val):
436 """
436 """
437 Setter that will always make sure we use unicode in app_settings_value
437 Setter that will always make sure we use unicode in app_settings_value
438
438
439 :param val:
439 :param val:
440 """
440 """
441 self._app_settings_value = safe_unicode(val)
441 self._app_settings_value = safe_unicode(val)
442
442
443 @hybrid_property
443 @hybrid_property
444 def app_settings_type(self):
444 def app_settings_type(self):
445 return self._app_settings_type
445 return self._app_settings_type
446
446
447 @app_settings_type.setter
447 @app_settings_type.setter
448 def app_settings_type(self, val):
448 def app_settings_type(self, val):
449 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
449 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
450 if val not in SETTINGS_TYPES:
450 if val not in SETTINGS_TYPES:
451 raise Exception('type must be one of %s got %s'
451 raise Exception('type must be one of %s got %s'
452 % (SETTINGS_TYPES.keys(), val))
452 % (SETTINGS_TYPES.keys(), val))
453 self._app_settings_type = val
453 self._app_settings_type = val
454
454
455 def __unicode__(self):
455 def __unicode__(self):
456 return u"<%s('%s:%s:%s[%s]')>" % (
456 return u"<%s('%s:%s:%s[%s]')>" % (
457 self.__class__.__name__, self.repository.repo_name,
457 self.__class__.__name__, self.repository.repo_name,
458 self.app_settings_name, self.app_settings_value,
458 self.app_settings_name, self.app_settings_value,
459 self.app_settings_type
459 self.app_settings_type
460 )
460 )
461
461
462
462
463 class RepoRhodeCodeUi(Base, BaseModel):
463 class RepoRhodeCodeUi(Base, BaseModel):
464 __tablename__ = 'repo_rhodecode_ui'
464 __tablename__ = 'repo_rhodecode_ui'
465 __table_args__ = (
465 __table_args__ = (
466 UniqueConstraint(
466 UniqueConstraint(
467 'repository_id', 'ui_section', 'ui_key',
467 'repository_id', 'ui_section', 'ui_key',
468 name='uq_repo_rhodecode_ui_repository_id_section_key'),
468 name='uq_repo_rhodecode_ui_repository_id_section_key'),
469 {'extend_existing': True, 'mysql_engine': 'InnoDB',
469 {'extend_existing': True, 'mysql_engine': 'InnoDB',
470 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
470 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
471 )
471 )
472
472
473 repository_id = Column(
473 repository_id = Column(
474 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
474 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
475 nullable=False)
475 nullable=False)
476 ui_id = Column(
476 ui_id = Column(
477 "ui_id", Integer(), nullable=False, unique=True, default=None,
477 "ui_id", Integer(), nullable=False, unique=True, default=None,
478 primary_key=True)
478 primary_key=True)
479 ui_section = Column(
479 ui_section = Column(
480 "ui_section", String(255), nullable=True, unique=None, default=None)
480 "ui_section", String(255), nullable=True, unique=None, default=None)
481 ui_key = Column(
481 ui_key = Column(
482 "ui_key", String(255), nullable=True, unique=None, default=None)
482 "ui_key", String(255), nullable=True, unique=None, default=None)
483 ui_value = Column(
483 ui_value = Column(
484 "ui_value", String(255), nullable=True, unique=None, default=None)
484 "ui_value", String(255), nullable=True, unique=None, default=None)
485 ui_active = Column(
485 ui_active = Column(
486 "ui_active", Boolean(), nullable=True, unique=None, default=True)
486 "ui_active", Boolean(), nullable=True, unique=None, default=True)
487
487
488 repository = relationship('Repository')
488 repository = relationship('Repository')
489
489
490 def __repr__(self):
490 def __repr__(self):
491 return '<%s[%s:%s]%s=>%s]>' % (
491 return '<%s[%s:%s]%s=>%s]>' % (
492 self.__class__.__name__, self.repository.repo_name,
492 self.__class__.__name__, self.repository.repo_name,
493 self.ui_section, self.ui_key, self.ui_value)
493 self.ui_section, self.ui_key, self.ui_value)
494
494
495
495
496 class User(Base, BaseModel):
496 class User(Base, BaseModel):
497 __tablename__ = 'users'
497 __tablename__ = 'users'
498 __table_args__ = (
498 __table_args__ = (
499 UniqueConstraint('username'), UniqueConstraint('email'),
499 UniqueConstraint('username'), UniqueConstraint('email'),
500 Index('u_username_idx', 'username'),
500 Index('u_username_idx', 'username'),
501 Index('u_email_idx', 'email'),
501 Index('u_email_idx', 'email'),
502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
503 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
503 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
504 )
504 )
505 DEFAULT_USER = 'default'
505 DEFAULT_USER = 'default'
506 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
506 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
507 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
507 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
508
508
509 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
509 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
510 username = Column("username", String(255), nullable=True, unique=None, default=None)
510 username = Column("username", String(255), nullable=True, unique=None, default=None)
511 password = Column("password", String(255), nullable=True, unique=None, default=None)
511 password = Column("password", String(255), nullable=True, unique=None, default=None)
512 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
512 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
513 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
513 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
514 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
514 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
515 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
515 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
516 _email = Column("email", String(255), nullable=True, unique=None, default=None)
516 _email = Column("email", String(255), nullable=True, unique=None, default=None)
517 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
517 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
518 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
518 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
519
519
520 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
520 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
521 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
521 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
522 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
522 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
523 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
523 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
524 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
524 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
525 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
525 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
526
526
527 user_log = relationship('UserLog')
527 user_log = relationship('UserLog')
528 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
528 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
529
529
530 repositories = relationship('Repository')
530 repositories = relationship('Repository')
531 repository_groups = relationship('RepoGroup')
531 repository_groups = relationship('RepoGroup')
532 user_groups = relationship('UserGroup')
532 user_groups = relationship('UserGroup')
533
533
534 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
534 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
535 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
535 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
536
536
537 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
537 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
538 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
538 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
539 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
539 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
540
540
541 group_member = relationship('UserGroupMember', cascade='all')
541 group_member = relationship('UserGroupMember', cascade='all')
542
542
543 notifications = relationship('UserNotification', cascade='all')
543 notifications = relationship('UserNotification', cascade='all')
544 # notifications assigned to this user
544 # notifications assigned to this user
545 user_created_notifications = relationship('Notification', cascade='all')
545 user_created_notifications = relationship('Notification', cascade='all')
546 # comments created by this user
546 # comments created by this user
547 user_comments = relationship('ChangesetComment', cascade='all')
547 user_comments = relationship('ChangesetComment', cascade='all')
548 # user profile extra info
548 # user profile extra info
549 user_emails = relationship('UserEmailMap', cascade='all')
549 user_emails = relationship('UserEmailMap', cascade='all')
550 user_ip_map = relationship('UserIpMap', cascade='all')
550 user_ip_map = relationship('UserIpMap', cascade='all')
551 user_auth_tokens = relationship('UserApiKeys', cascade='all')
551 user_auth_tokens = relationship('UserApiKeys', cascade='all')
552 # gists
552 # gists
553 user_gists = relationship('Gist', cascade='all')
553 user_gists = relationship('Gist', cascade='all')
554 # user pull requests
554 # user pull requests
555 user_pull_requests = relationship('PullRequest', cascade='all')
555 user_pull_requests = relationship('PullRequest', cascade='all')
556 # external identities
556 # external identities
557 extenal_identities = relationship(
557 extenal_identities = relationship(
558 'ExternalIdentity',
558 'ExternalIdentity',
559 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
559 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
560 cascade='all')
560 cascade='all')
561
561
562 def __unicode__(self):
562 def __unicode__(self):
563 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
563 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
564 self.user_id, self.username)
564 self.user_id, self.username)
565
565
566 @hybrid_property
566 @hybrid_property
567 def email(self):
567 def email(self):
568 return self._email
568 return self._email
569
569
570 @email.setter
570 @email.setter
571 def email(self, val):
571 def email(self, val):
572 self._email = val.lower() if val else None
572 self._email = val.lower() if val else None
573
573
574 @hybrid_property
574 @hybrid_property
575 def api_key(self):
575 def api_key(self):
576 """
576 """
577 Fetch if exist an auth-token with role ALL connected to this user
577 Fetch if exist an auth-token with role ALL connected to this user
578 """
578 """
579 user_auth_token = UserApiKeys.query()\
579 user_auth_token = UserApiKeys.query()\
580 .filter(UserApiKeys.user_id == self.user_id)\
580 .filter(UserApiKeys.user_id == self.user_id)\
581 .filter(or_(UserApiKeys.expires == -1,
581 .filter(or_(UserApiKeys.expires == -1,
582 UserApiKeys.expires >= time.time()))\
582 UserApiKeys.expires >= time.time()))\
583 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
583 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
584 if user_auth_token:
584 if user_auth_token:
585 user_auth_token = user_auth_token.api_key
585 user_auth_token = user_auth_token.api_key
586
586
587 return user_auth_token
587 return user_auth_token
588
588
589 @api_key.setter
589 @api_key.setter
590 def api_key(self, val):
590 def api_key(self, val):
591 # don't allow to set API key this is deprecated for now
591 # don't allow to set API key this is deprecated for now
592 self._api_key = None
592 self._api_key = None
593
593
594 @property
594 @property
595 def firstname(self):
595 def firstname(self):
596 # alias for future
596 # alias for future
597 return self.name
597 return self.name
598
598
599 @property
599 @property
600 def emails(self):
600 def emails(self):
601 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
601 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
602 return [self.email] + [x.email for x in other]
602 return [self.email] + [x.email for x in other]
603
603
604 @property
604 @property
605 def auth_tokens(self):
605 def auth_tokens(self):
606 return [x.api_key for x in self.extra_auth_tokens]
606 return [x.api_key for x in self.extra_auth_tokens]
607
607
608 @property
608 @property
609 def extra_auth_tokens(self):
609 def extra_auth_tokens(self):
610 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
610 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
611
611
612 @property
612 @property
613 def feed_token(self):
613 def feed_token(self):
614 return self.get_feed_token()
614 return self.get_feed_token()
615
615
616 def get_feed_token(self):
616 def get_feed_token(self):
617 feed_tokens = UserApiKeys.query()\
617 feed_tokens = UserApiKeys.query()\
618 .filter(UserApiKeys.user == self)\
618 .filter(UserApiKeys.user == self)\
619 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
619 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
620 .all()
620 .all()
621 if feed_tokens:
621 if feed_tokens:
622 return feed_tokens[0].api_key
622 return feed_tokens[0].api_key
623 return 'NO_FEED_TOKEN_AVAILABLE'
623 return 'NO_FEED_TOKEN_AVAILABLE'
624
624
625 @classmethod
625 @classmethod
626 def extra_valid_auth_tokens(cls, user, role=None):
626 def extra_valid_auth_tokens(cls, user, role=None):
627 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
627 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
628 .filter(or_(UserApiKeys.expires == -1,
628 .filter(or_(UserApiKeys.expires == -1,
629 UserApiKeys.expires >= time.time()))
629 UserApiKeys.expires >= time.time()))
630 if role:
630 if role:
631 tokens = tokens.filter(or_(UserApiKeys.role == role,
631 tokens = tokens.filter(or_(UserApiKeys.role == role,
632 UserApiKeys.role == UserApiKeys.ROLE_ALL))
632 UserApiKeys.role == UserApiKeys.ROLE_ALL))
633 return tokens.all()
633 return tokens.all()
634
634
635 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
635 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
636 from rhodecode.lib import auth
636 from rhodecode.lib import auth
637
637
638 log.debug('Trying to authenticate user: %s via auth-token, '
638 log.debug('Trying to authenticate user: %s via auth-token, '
639 'and roles: %s', self, roles)
639 'and roles: %s', self, roles)
640
640
641 if not auth_token:
641 if not auth_token:
642 return False
642 return False
643
643
644 crypto_backend = auth.crypto_backend()
644 crypto_backend = auth.crypto_backend()
645
645
646 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
646 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
647 tokens_q = UserApiKeys.query()\
647 tokens_q = UserApiKeys.query()\
648 .filter(UserApiKeys.user_id == self.user_id)\
648 .filter(UserApiKeys.user_id == self.user_id)\
649 .filter(or_(UserApiKeys.expires == -1,
649 .filter(or_(UserApiKeys.expires == -1,
650 UserApiKeys.expires >= time.time()))
650 UserApiKeys.expires >= time.time()))
651
651
652 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
652 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
653
653
654 plain_tokens = []
654 plain_tokens = []
655 hash_tokens = []
655 hash_tokens = []
656
656
657 for token in tokens_q.all():
657 for token in tokens_q.all():
658 # verify scope first
658 # verify scope first
659 if token.repo_id:
659 if token.repo_id:
660 # token has a scope, we need to verify it
660 # token has a scope, we need to verify it
661 if scope_repo_id != token.repo_id:
661 if scope_repo_id != token.repo_id:
662 log.debug(
662 log.debug(
663 'Scope mismatch: token has a set repo scope: %s, '
663 'Scope mismatch: token has a set repo scope: %s, '
664 'and calling scope is:%s, skipping further checks',
664 'and calling scope is:%s, skipping further checks',
665 token.repo, scope_repo_id)
665 token.repo, scope_repo_id)
666 # token has a scope, and it doesn't match, skip token
666 # token has a scope, and it doesn't match, skip token
667 continue
667 continue
668
668
669 if token.api_key.startswith(crypto_backend.ENC_PREF):
669 if token.api_key.startswith(crypto_backend.ENC_PREF):
670 hash_tokens.append(token.api_key)
670 hash_tokens.append(token.api_key)
671 else:
671 else:
672 plain_tokens.append(token.api_key)
672 plain_tokens.append(token.api_key)
673
673
674 is_plain_match = auth_token in plain_tokens
674 is_plain_match = auth_token in plain_tokens
675 if is_plain_match:
675 if is_plain_match:
676 return True
676 return True
677
677
678 for hashed in hash_tokens:
678 for hashed in hash_tokens:
679 # TODO(marcink): this is expensive to calculate, but most secure
679 # TODO(marcink): this is expensive to calculate, but most secure
680 match = crypto_backend.hash_check(auth_token, hashed)
680 match = crypto_backend.hash_check(auth_token, hashed)
681 if match:
681 if match:
682 return True
682 return True
683
683
684 return False
684 return False
685
685
686 @property
686 @property
687 def ip_addresses(self):
687 def ip_addresses(self):
688 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
688 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
689 return [x.ip_addr for x in ret]
689 return [x.ip_addr for x in ret]
690
690
691 @property
691 @property
692 def username_and_name(self):
692 def username_and_name(self):
693 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
693 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
694
694
695 @property
695 @property
696 def username_or_name_or_email(self):
696 def username_or_name_or_email(self):
697 full_name = self.full_name if self.full_name is not ' ' else None
697 full_name = self.full_name if self.full_name is not ' ' else None
698 return self.username or full_name or self.email
698 return self.username or full_name or self.email
699
699
700 @property
700 @property
701 def full_name(self):
701 def full_name(self):
702 return '%s %s' % (self.firstname, self.lastname)
702 return '%s %s' % (self.firstname, self.lastname)
703
703
704 @property
704 @property
705 def full_name_or_username(self):
705 def full_name_or_username(self):
706 return ('%s %s' % (self.firstname, self.lastname)
706 return ('%s %s' % (self.firstname, self.lastname)
707 if (self.firstname and self.lastname) else self.username)
707 if (self.firstname and self.lastname) else self.username)
708
708
709 @property
709 @property
710 def full_contact(self):
710 def full_contact(self):
711 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
711 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
712
712
713 @property
713 @property
714 def short_contact(self):
714 def short_contact(self):
715 return '%s %s' % (self.firstname, self.lastname)
715 return '%s %s' % (self.firstname, self.lastname)
716
716
717 @property
717 @property
718 def is_admin(self):
718 def is_admin(self):
719 return self.admin
719 return self.admin
720
720
721 @property
721 @property
722 def AuthUser(self):
722 def AuthUser(self):
723 """
723 """
724 Returns instance of AuthUser for this user
724 Returns instance of AuthUser for this user
725 """
725 """
726 from rhodecode.lib.auth import AuthUser
726 from rhodecode.lib.auth import AuthUser
727 return AuthUser(user_id=self.user_id, username=self.username)
727 return AuthUser(user_id=self.user_id, username=self.username)
728
728
729 @hybrid_property
729 @hybrid_property
730 def user_data(self):
730 def user_data(self):
731 if not self._user_data:
731 if not self._user_data:
732 return {}
732 return {}
733
733
734 try:
734 try:
735 return json.loads(self._user_data)
735 return json.loads(self._user_data)
736 except TypeError:
736 except TypeError:
737 return {}
737 return {}
738
738
739 @user_data.setter
739 @user_data.setter
740 def user_data(self, val):
740 def user_data(self, val):
741 if not isinstance(val, dict):
741 if not isinstance(val, dict):
742 raise Exception('user_data must be dict, got %s' % type(val))
742 raise Exception('user_data must be dict, got %s' % type(val))
743 try:
743 try:
744 self._user_data = json.dumps(val)
744 self._user_data = json.dumps(val)
745 except Exception:
745 except Exception:
746 log.error(traceback.format_exc())
746 log.error(traceback.format_exc())
747
747
748 @classmethod
748 @classmethod
749 def get_by_username(cls, username, case_insensitive=False,
749 def get_by_username(cls, username, case_insensitive=False,
750 cache=False, identity_cache=False):
750 cache=False, identity_cache=False):
751 session = Session()
751 session = Session()
752
752
753 if case_insensitive:
753 if case_insensitive:
754 q = cls.query().filter(
754 q = cls.query().filter(
755 func.lower(cls.username) == func.lower(username))
755 func.lower(cls.username) == func.lower(username))
756 else:
756 else:
757 q = cls.query().filter(cls.username == username)
757 q = cls.query().filter(cls.username == username)
758
758
759 if cache:
759 if cache:
760 if identity_cache:
760 if identity_cache:
761 val = cls.identity_cache(session, 'username', username)
761 val = cls.identity_cache(session, 'username', username)
762 if val:
762 if val:
763 return val
763 return val
764 else:
764 else:
765 cache_key = "get_user_by_name_%s" % _hash_key(username)
765 cache_key = "get_user_by_name_%s" % _hash_key(username)
766 q = q.options(
766 q = q.options(
767 FromCache("sql_cache_short", cache_key))
767 FromCache("sql_cache_short", cache_key))
768
768
769 return q.scalar()
769 return q.scalar()
770
770
771 @classmethod
771 @classmethod
772 def get_by_auth_token(cls, auth_token, cache=False):
772 def get_by_auth_token(cls, auth_token, cache=False):
773 q = UserApiKeys.query()\
773 q = UserApiKeys.query()\
774 .filter(UserApiKeys.api_key == auth_token)\
774 .filter(UserApiKeys.api_key == auth_token)\
775 .filter(or_(UserApiKeys.expires == -1,
775 .filter(or_(UserApiKeys.expires == -1,
776 UserApiKeys.expires >= time.time()))
776 UserApiKeys.expires >= time.time()))
777 if cache:
777 if cache:
778 q = q.options(
778 q = q.options(
779 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
779 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
780
780
781 match = q.first()
781 match = q.first()
782 if match:
782 if match:
783 return match.user
783 return match.user
784
784
785 @classmethod
785 @classmethod
786 def get_by_email(cls, email, case_insensitive=False, cache=False):
786 def get_by_email(cls, email, case_insensitive=False, cache=False):
787
787
788 if case_insensitive:
788 if case_insensitive:
789 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
789 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
790
790
791 else:
791 else:
792 q = cls.query().filter(cls.email == email)
792 q = cls.query().filter(cls.email == email)
793
793
794 email_key = _hash_key(email)
794 email_key = _hash_key(email)
795 if cache:
795 if cache:
796 q = q.options(
796 q = q.options(
797 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
797 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
798
798
799 ret = q.scalar()
799 ret = q.scalar()
800 if ret is None:
800 if ret is None:
801 q = UserEmailMap.query()
801 q = UserEmailMap.query()
802 # try fetching in alternate email map
802 # try fetching in alternate email map
803 if case_insensitive:
803 if case_insensitive:
804 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
804 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
805 else:
805 else:
806 q = q.filter(UserEmailMap.email == email)
806 q = q.filter(UserEmailMap.email == email)
807 q = q.options(joinedload(UserEmailMap.user))
807 q = q.options(joinedload(UserEmailMap.user))
808 if cache:
808 if cache:
809 q = q.options(
809 q = q.options(
810 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
810 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
811 ret = getattr(q.scalar(), 'user', None)
811 ret = getattr(q.scalar(), 'user', None)
812
812
813 return ret
813 return ret
814
814
815 @classmethod
815 @classmethod
816 def get_from_cs_author(cls, author):
816 def get_from_cs_author(cls, author):
817 """
817 """
818 Tries to get User objects out of commit author string
818 Tries to get User objects out of commit author string
819
819
820 :param author:
820 :param author:
821 """
821 """
822 from rhodecode.lib.helpers import email, author_name
822 from rhodecode.lib.helpers import email, author_name
823 # Valid email in the attribute passed, see if they're in the system
823 # Valid email in the attribute passed, see if they're in the system
824 _email = email(author)
824 _email = email(author)
825 if _email:
825 if _email:
826 user = cls.get_by_email(_email, case_insensitive=True)
826 user = cls.get_by_email(_email, case_insensitive=True)
827 if user:
827 if user:
828 return user
828 return user
829 # Maybe we can match by username?
829 # Maybe we can match by username?
830 _author = author_name(author)
830 _author = author_name(author)
831 user = cls.get_by_username(_author, case_insensitive=True)
831 user = cls.get_by_username(_author, case_insensitive=True)
832 if user:
832 if user:
833 return user
833 return user
834
834
835 def update_userdata(self, **kwargs):
835 def update_userdata(self, **kwargs):
836 usr = self
836 usr = self
837 old = usr.user_data
837 old = usr.user_data
838 old.update(**kwargs)
838 old.update(**kwargs)
839 usr.user_data = old
839 usr.user_data = old
840 Session().add(usr)
840 Session().add(usr)
841 log.debug('updated userdata with ', kwargs)
841 log.debug('updated userdata with ', kwargs)
842
842
843 def update_lastlogin(self):
843 def update_lastlogin(self):
844 """Update user lastlogin"""
844 """Update user lastlogin"""
845 self.last_login = datetime.datetime.now()
845 self.last_login = datetime.datetime.now()
846 Session().add(self)
846 Session().add(self)
847 log.debug('updated user %s lastlogin', self.username)
847 log.debug('updated user %s lastlogin', self.username)
848
848
849 def update_lastactivity(self):
849 def update_lastactivity(self):
850 """Update user lastactivity"""
850 """Update user lastactivity"""
851 self.last_activity = datetime.datetime.now()
851 self.last_activity = datetime.datetime.now()
852 Session().add(self)
852 Session().add(self)
853 log.debug('updated user %s lastactivity', self.username)
853 log.debug('updated user %s lastactivity', self.username)
854
854
855 def update_password(self, new_password):
855 def update_password(self, new_password):
856 from rhodecode.lib.auth import get_crypt_password
856 from rhodecode.lib.auth import get_crypt_password
857
857
858 self.password = get_crypt_password(new_password)
858 self.password = get_crypt_password(new_password)
859 Session().add(self)
859 Session().add(self)
860
860
861 @classmethod
861 @classmethod
862 def get_first_super_admin(cls):
862 def get_first_super_admin(cls):
863 user = User.query().filter(User.admin == true()).first()
863 user = User.query().filter(User.admin == true()).first()
864 if user is None:
864 if user is None:
865 raise Exception('FATAL: Missing administrative account!')
865 raise Exception('FATAL: Missing administrative account!')
866 return user
866 return user
867
867
868 @classmethod
868 @classmethod
869 def get_all_super_admins(cls):
869 def get_all_super_admins(cls):
870 """
870 """
871 Returns all admin accounts sorted by username
871 Returns all admin accounts sorted by username
872 """
872 """
873 return User.query().filter(User.admin == true())\
873 return User.query().filter(User.admin == true())\
874 .order_by(User.username.asc()).all()
874 .order_by(User.username.asc()).all()
875
875
876 @classmethod
876 @classmethod
877 def get_default_user(cls, cache=False, refresh=False):
877 def get_default_user(cls, cache=False, refresh=False):
878 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
878 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
879 if user is None:
879 if user is None:
880 raise Exception('FATAL: Missing default account!')
880 raise Exception('FATAL: Missing default account!')
881 if refresh:
881 if refresh:
882 # The default user might be based on outdated state which
882 # The default user might be based on outdated state which
883 # has been loaded from the cache.
883 # has been loaded from the cache.
884 # A call to refresh() ensures that the
884 # A call to refresh() ensures that the
885 # latest state from the database is used.
885 # latest state from the database is used.
886 Session().refresh(user)
886 Session().refresh(user)
887 return user
887 return user
888
888
889 def _get_default_perms(self, user, suffix=''):
889 def _get_default_perms(self, user, suffix=''):
890 from rhodecode.model.permission import PermissionModel
890 from rhodecode.model.permission import PermissionModel
891 return PermissionModel().get_default_perms(user.user_perms, suffix)
891 return PermissionModel().get_default_perms(user.user_perms, suffix)
892
892
893 def get_default_perms(self, suffix=''):
893 def get_default_perms(self, suffix=''):
894 return self._get_default_perms(self, suffix)
894 return self._get_default_perms(self, suffix)
895
895
896 def get_api_data(self, include_secrets=False, details='full'):
896 def get_api_data(self, include_secrets=False, details='full'):
897 """
897 """
898 Common function for generating user related data for API
898 Common function for generating user related data for API
899
899
900 :param include_secrets: By default secrets in the API data will be replaced
900 :param include_secrets: By default secrets in the API data will be replaced
901 by a placeholder value to prevent exposing this data by accident. In case
901 by a placeholder value to prevent exposing this data by accident. In case
902 this data shall be exposed, set this flag to ``True``.
902 this data shall be exposed, set this flag to ``True``.
903
903
904 :param details: details can be 'basic|full' basic gives only a subset of
904 :param details: details can be 'basic|full' basic gives only a subset of
905 the available user information that includes user_id, name and emails.
905 the available user information that includes user_id, name and emails.
906 """
906 """
907 user = self
907 user = self
908 user_data = self.user_data
908 user_data = self.user_data
909 data = {
909 data = {
910 'user_id': user.user_id,
910 'user_id': user.user_id,
911 'username': user.username,
911 'username': user.username,
912 'firstname': user.name,
912 'firstname': user.name,
913 'lastname': user.lastname,
913 'lastname': user.lastname,
914 'email': user.email,
914 'email': user.email,
915 'emails': user.emails,
915 'emails': user.emails,
916 }
916 }
917 if details == 'basic':
917 if details == 'basic':
918 return data
918 return data
919
919
920 api_key_length = 40
920 api_key_length = 40
921 api_key_replacement = '*' * api_key_length
921 api_key_replacement = '*' * api_key_length
922
922
923 extras = {
923 extras = {
924 'api_keys': [api_key_replacement],
924 'api_keys': [api_key_replacement],
925 'auth_tokens': [api_key_replacement],
925 'auth_tokens': [api_key_replacement],
926 'active': user.active,
926 'active': user.active,
927 'admin': user.admin,
927 'admin': user.admin,
928 'extern_type': user.extern_type,
928 'extern_type': user.extern_type,
929 'extern_name': user.extern_name,
929 'extern_name': user.extern_name,
930 'last_login': user.last_login,
930 'last_login': user.last_login,
931 'last_activity': user.last_activity,
931 'last_activity': user.last_activity,
932 'ip_addresses': user.ip_addresses,
932 'ip_addresses': user.ip_addresses,
933 'language': user_data.get('language')
933 'language': user_data.get('language')
934 }
934 }
935 data.update(extras)
935 data.update(extras)
936
936
937 if include_secrets:
937 if include_secrets:
938 data['api_keys'] = user.auth_tokens
938 data['api_keys'] = user.auth_tokens
939 data['auth_tokens'] = user.extra_auth_tokens
939 data['auth_tokens'] = user.extra_auth_tokens
940 return data
940 return data
941
941
942 def __json__(self):
942 def __json__(self):
943 data = {
943 data = {
944 'full_name': self.full_name,
944 'full_name': self.full_name,
945 'full_name_or_username': self.full_name_or_username,
945 'full_name_or_username': self.full_name_or_username,
946 'short_contact': self.short_contact,
946 'short_contact': self.short_contact,
947 'full_contact': self.full_contact,
947 'full_contact': self.full_contact,
948 }
948 }
949 data.update(self.get_api_data())
949 data.update(self.get_api_data())
950 return data
950 return data
951
951
952
952
953 class UserApiKeys(Base, BaseModel):
953 class UserApiKeys(Base, BaseModel):
954 __tablename__ = 'user_api_keys'
954 __tablename__ = 'user_api_keys'
955 __table_args__ = (
955 __table_args__ = (
956 Index('uak_api_key_idx', 'api_key'),
956 Index('uak_api_key_idx', 'api_key'),
957 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
957 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
958 UniqueConstraint('api_key'),
958 UniqueConstraint('api_key'),
959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
961 )
961 )
962 __mapper_args__ = {}
962 __mapper_args__ = {}
963
963
964 # ApiKey role
964 # ApiKey role
965 ROLE_ALL = 'token_role_all'
965 ROLE_ALL = 'token_role_all'
966 ROLE_HTTP = 'token_role_http'
966 ROLE_HTTP = 'token_role_http'
967 ROLE_VCS = 'token_role_vcs'
967 ROLE_VCS = 'token_role_vcs'
968 ROLE_API = 'token_role_api'
968 ROLE_API = 'token_role_api'
969 ROLE_FEED = 'token_role_feed'
969 ROLE_FEED = 'token_role_feed'
970 ROLE_PASSWORD_RESET = 'token_password_reset'
970 ROLE_PASSWORD_RESET = 'token_password_reset'
971
971
972 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
972 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
973
973
974 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
974 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
975 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
975 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
976 api_key = Column("api_key", String(255), nullable=False, unique=True)
976 api_key = Column("api_key", String(255), nullable=False, unique=True)
977 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
977 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
978 expires = Column('expires', Float(53), nullable=False)
978 expires = Column('expires', Float(53), nullable=False)
979 role = Column('role', String(255), nullable=True)
979 role = Column('role', String(255), nullable=True)
980 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
980 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
981
981
982 # scope columns
982 # scope columns
983 repo_id = Column(
983 repo_id = Column(
984 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
984 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
985 nullable=True, unique=None, default=None)
985 nullable=True, unique=None, default=None)
986 repo = relationship('Repository', lazy='joined')
986 repo = relationship('Repository', lazy='joined')
987
987
988 repo_group_id = Column(
988 repo_group_id = Column(
989 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
989 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
990 nullable=True, unique=None, default=None)
990 nullable=True, unique=None, default=None)
991 repo_group = relationship('RepoGroup', lazy='joined')
991 repo_group = relationship('RepoGroup', lazy='joined')
992
992
993 user = relationship('User', lazy='joined')
993 user = relationship('User', lazy='joined')
994
994
995 def __unicode__(self):
995 def __unicode__(self):
996 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
996 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
997
997
998 def __json__(self):
998 def __json__(self):
999 data = {
999 data = {
1000 'auth_token': self.api_key,
1000 'auth_token': self.api_key,
1001 'role': self.role,
1001 'role': self.role,
1002 'scope': self.scope_humanized,
1002 'scope': self.scope_humanized,
1003 'expired': self.expired
1003 'expired': self.expired
1004 }
1004 }
1005 return data
1005 return data
1006
1006
1007 @property
1007 @property
1008 def expired(self):
1008 def expired(self):
1009 if self.expires == -1:
1009 if self.expires == -1:
1010 return False
1010 return False
1011 return time.time() > self.expires
1011 return time.time() > self.expires
1012
1012
1013 @classmethod
1013 @classmethod
1014 def _get_role_name(cls, role):
1014 def _get_role_name(cls, role):
1015 return {
1015 return {
1016 cls.ROLE_ALL: _('all'),
1016 cls.ROLE_ALL: _('all'),
1017 cls.ROLE_HTTP: _('http/web interface'),
1017 cls.ROLE_HTTP: _('http/web interface'),
1018 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1018 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1019 cls.ROLE_API: _('api calls'),
1019 cls.ROLE_API: _('api calls'),
1020 cls.ROLE_FEED: _('feed access'),
1020 cls.ROLE_FEED: _('feed access'),
1021 }.get(role, role)
1021 }.get(role, role)
1022
1022
1023 @property
1023 @property
1024 def role_humanized(self):
1024 def role_humanized(self):
1025 return self._get_role_name(self.role)
1025 return self._get_role_name(self.role)
1026
1026
1027 def _get_scope(self):
1027 def _get_scope(self):
1028 if self.repo:
1028 if self.repo:
1029 return repr(self.repo)
1029 return repr(self.repo)
1030 if self.repo_group:
1030 if self.repo_group:
1031 return repr(self.repo_group) + ' (recursive)'
1031 return repr(self.repo_group) + ' (recursive)'
1032 return 'global'
1032 return 'global'
1033
1033
1034 @property
1034 @property
1035 def scope_humanized(self):
1035 def scope_humanized(self):
1036 return self._get_scope()
1036 return self._get_scope()
1037
1037
1038
1038
1039 class UserEmailMap(Base, BaseModel):
1039 class UserEmailMap(Base, BaseModel):
1040 __tablename__ = 'user_email_map'
1040 __tablename__ = 'user_email_map'
1041 __table_args__ = (
1041 __table_args__ = (
1042 Index('uem_email_idx', 'email'),
1042 Index('uem_email_idx', 'email'),
1043 UniqueConstraint('email'),
1043 UniqueConstraint('email'),
1044 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1044 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1045 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1045 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1046 )
1046 )
1047 __mapper_args__ = {}
1047 __mapper_args__ = {}
1048
1048
1049 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1049 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1051 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1051 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1052 user = relationship('User', lazy='joined')
1052 user = relationship('User', lazy='joined')
1053
1053
1054 @validates('_email')
1054 @validates('_email')
1055 def validate_email(self, key, email):
1055 def validate_email(self, key, email):
1056 # check if this email is not main one
1056 # check if this email is not main one
1057 main_email = Session().query(User).filter(User.email == email).scalar()
1057 main_email = Session().query(User).filter(User.email == email).scalar()
1058 if main_email is not None:
1058 if main_email is not None:
1059 raise AttributeError('email %s is present is user table' % email)
1059 raise AttributeError('email %s is present is user table' % email)
1060 return email
1060 return email
1061
1061
1062 @hybrid_property
1062 @hybrid_property
1063 def email(self):
1063 def email(self):
1064 return self._email
1064 return self._email
1065
1065
1066 @email.setter
1066 @email.setter
1067 def email(self, val):
1067 def email(self, val):
1068 self._email = val.lower() if val else None
1068 self._email = val.lower() if val else None
1069
1069
1070
1070
1071 class UserIpMap(Base, BaseModel):
1071 class UserIpMap(Base, BaseModel):
1072 __tablename__ = 'user_ip_map'
1072 __tablename__ = 'user_ip_map'
1073 __table_args__ = (
1073 __table_args__ = (
1074 UniqueConstraint('user_id', 'ip_addr'),
1074 UniqueConstraint('user_id', 'ip_addr'),
1075 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1075 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1076 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1076 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1077 )
1077 )
1078 __mapper_args__ = {}
1078 __mapper_args__ = {}
1079
1079
1080 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1080 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1081 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1081 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1082 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1082 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1083 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1083 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1084 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1084 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1085 user = relationship('User', lazy='joined')
1085 user = relationship('User', lazy='joined')
1086
1086
1087 @classmethod
1087 @classmethod
1088 def _get_ip_range(cls, ip_addr):
1088 def _get_ip_range(cls, ip_addr):
1089 net = ipaddress.ip_network(ip_addr, strict=False)
1089 net = ipaddress.ip_network(ip_addr, strict=False)
1090 return [str(net.network_address), str(net.broadcast_address)]
1090 return [str(net.network_address), str(net.broadcast_address)]
1091
1091
1092 def __json__(self):
1092 def __json__(self):
1093 return {
1093 return {
1094 'ip_addr': self.ip_addr,
1094 'ip_addr': self.ip_addr,
1095 'ip_range': self._get_ip_range(self.ip_addr),
1095 'ip_range': self._get_ip_range(self.ip_addr),
1096 }
1096 }
1097
1097
1098 def __unicode__(self):
1098 def __unicode__(self):
1099 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1099 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1100 self.user_id, self.ip_addr)
1100 self.user_id, self.ip_addr)
1101
1101
1102
1102
1103 class UserLog(Base, BaseModel):
1103 class UserLog(Base, BaseModel):
1104 __tablename__ = 'user_logs'
1104 __tablename__ = 'user_logs'
1105 __table_args__ = (
1105 __table_args__ = (
1106 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1106 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1107 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1107 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1108 )
1108 )
1109 VERSION_1 = 'v1'
1109 VERSION_1 = 'v1'
1110 VERSION_2 = 'v2'
1110 VERSION_2 = 'v2'
1111 VERSIONS = [VERSION_1, VERSION_2]
1111 VERSIONS = [VERSION_1, VERSION_2]
1112
1112
1113 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1113 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1114 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1114 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1115 username = Column("username", String(255), nullable=True, unique=None, default=None)
1115 username = Column("username", String(255), nullable=True, unique=None, default=None)
1116 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1116 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1117 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1117 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1118 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1118 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1119 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1119 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1120 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1120 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1121
1121
1122 version = Column("version", String(255), nullable=True, default=VERSION_1)
1122 version = Column("version", String(255), nullable=True, default=VERSION_1)
1123 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1123 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1124 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1124 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1125
1125
1126 def __unicode__(self):
1126 def __unicode__(self):
1127 return u"<%s('id:%s:%s')>" % (
1127 return u"<%s('id:%s:%s')>" % (
1128 self.__class__.__name__, self.repository_name, self.action)
1128 self.__class__.__name__, self.repository_name, self.action)
1129
1129
1130 def __json__(self):
1130 def __json__(self):
1131 return {
1131 return {
1132 'user_id': self.user_id,
1132 'user_id': self.user_id,
1133 'username': self.username,
1133 'username': self.username,
1134 'repository_id': self.repository_id,
1134 'repository_id': self.repository_id,
1135 'repository_name': self.repository_name,
1135 'repository_name': self.repository_name,
1136 'user_ip': self.user_ip,
1136 'user_ip': self.user_ip,
1137 'action_date': self.action_date,
1137 'action_date': self.action_date,
1138 'action': self.action,
1138 'action': self.action,
1139 }
1139 }
1140
1140
1141 @property
1141 @property
1142 def action_as_day(self):
1142 def action_as_day(self):
1143 return datetime.date(*self.action_date.timetuple()[:3])
1143 return datetime.date(*self.action_date.timetuple()[:3])
1144
1144
1145 user = relationship('User')
1145 user = relationship('User')
1146 repository = relationship('Repository', cascade='')
1146 repository = relationship('Repository', cascade='')
1147
1147
1148
1148
1149 class UserGroup(Base, BaseModel):
1149 class UserGroup(Base, BaseModel):
1150 __tablename__ = 'users_groups'
1150 __tablename__ = 'users_groups'
1151 __table_args__ = (
1151 __table_args__ = (
1152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1153 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1153 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1154 )
1154 )
1155
1155
1156 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1156 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1157 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1157 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1158 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1158 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1159 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1159 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1160 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1160 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1161 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1161 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1162 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1162 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1163 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1163 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1164
1164
1165 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1165 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1166 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1166 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1167 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1167 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1168 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1168 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1169 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1169 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1170 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1170 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1171
1171
1172 user = relationship('User')
1172 user = relationship('User')
1173
1173
1174 @hybrid_property
1174 @hybrid_property
1175 def group_data(self):
1175 def group_data(self):
1176 if not self._group_data:
1176 if not self._group_data:
1177 return {}
1177 return {}
1178
1178
1179 try:
1179 try:
1180 return json.loads(self._group_data)
1180 return json.loads(self._group_data)
1181 except TypeError:
1181 except TypeError:
1182 return {}
1182 return {}
1183
1183
1184 @group_data.setter
1184 @group_data.setter
1185 def group_data(self, val):
1185 def group_data(self, val):
1186 try:
1186 try:
1187 self._group_data = json.dumps(val)
1187 self._group_data = json.dumps(val)
1188 except Exception:
1188 except Exception:
1189 log.error(traceback.format_exc())
1189 log.error(traceback.format_exc())
1190
1190
1191 def __unicode__(self):
1191 def __unicode__(self):
1192 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1192 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1193 self.users_group_id,
1193 self.users_group_id,
1194 self.users_group_name)
1194 self.users_group_name)
1195
1195
1196 @classmethod
1196 @classmethod
1197 def get_by_group_name(cls, group_name, cache=False,
1197 def get_by_group_name(cls, group_name, cache=False,
1198 case_insensitive=False):
1198 case_insensitive=False):
1199 if case_insensitive:
1199 if case_insensitive:
1200 q = cls.query().filter(func.lower(cls.users_group_name) ==
1200 q = cls.query().filter(func.lower(cls.users_group_name) ==
1201 func.lower(group_name))
1201 func.lower(group_name))
1202
1202
1203 else:
1203 else:
1204 q = cls.query().filter(cls.users_group_name == group_name)
1204 q = cls.query().filter(cls.users_group_name == group_name)
1205 if cache:
1205 if cache:
1206 q = q.options(
1206 q = q.options(
1207 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1207 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1208 return q.scalar()
1208 return q.scalar()
1209
1209
1210 @classmethod
1210 @classmethod
1211 def get(cls, user_group_id, cache=False):
1211 def get(cls, user_group_id, cache=False):
1212 user_group = cls.query()
1212 user_group = cls.query()
1213 if cache:
1213 if cache:
1214 user_group = user_group.options(
1214 user_group = user_group.options(
1215 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1215 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1216 return user_group.get(user_group_id)
1216 return user_group.get(user_group_id)
1217
1217
1218 def permissions(self, with_admins=True, with_owner=True):
1218 def permissions(self, with_admins=True, with_owner=True):
1219 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1219 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1220 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1220 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1221 joinedload(UserUserGroupToPerm.user),
1221 joinedload(UserUserGroupToPerm.user),
1222 joinedload(UserUserGroupToPerm.permission),)
1222 joinedload(UserUserGroupToPerm.permission),)
1223
1223
1224 # get owners and admins and permissions. We do a trick of re-writing
1224 # get owners and admins and permissions. We do a trick of re-writing
1225 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1225 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1226 # has a global reference and changing one object propagates to all
1226 # has a global reference and changing one object propagates to all
1227 # others. This means if admin is also an owner admin_row that change
1227 # others. This means if admin is also an owner admin_row that change
1228 # would propagate to both objects
1228 # would propagate to both objects
1229 perm_rows = []
1229 perm_rows = []
1230 for _usr in q.all():
1230 for _usr in q.all():
1231 usr = AttributeDict(_usr.user.get_dict())
1231 usr = AttributeDict(_usr.user.get_dict())
1232 usr.permission = _usr.permission.permission_name
1232 usr.permission = _usr.permission.permission_name
1233 perm_rows.append(usr)
1233 perm_rows.append(usr)
1234
1234
1235 # filter the perm rows by 'default' first and then sort them by
1235 # filter the perm rows by 'default' first and then sort them by
1236 # admin,write,read,none permissions sorted again alphabetically in
1236 # admin,write,read,none permissions sorted again alphabetically in
1237 # each group
1237 # each group
1238 perm_rows = sorted(perm_rows, key=display_sort)
1238 perm_rows = sorted(perm_rows, key=display_sort)
1239
1239
1240 _admin_perm = 'usergroup.admin'
1240 _admin_perm = 'usergroup.admin'
1241 owner_row = []
1241 owner_row = []
1242 if with_owner:
1242 if with_owner:
1243 usr = AttributeDict(self.user.get_dict())
1243 usr = AttributeDict(self.user.get_dict())
1244 usr.owner_row = True
1244 usr.owner_row = True
1245 usr.permission = _admin_perm
1245 usr.permission = _admin_perm
1246 owner_row.append(usr)
1246 owner_row.append(usr)
1247
1247
1248 super_admin_rows = []
1248 super_admin_rows = []
1249 if with_admins:
1249 if with_admins:
1250 for usr in User.get_all_super_admins():
1250 for usr in User.get_all_super_admins():
1251 # if this admin is also owner, don't double the record
1251 # if this admin is also owner, don't double the record
1252 if usr.user_id == owner_row[0].user_id:
1252 if usr.user_id == owner_row[0].user_id:
1253 owner_row[0].admin_row = True
1253 owner_row[0].admin_row = True
1254 else:
1254 else:
1255 usr = AttributeDict(usr.get_dict())
1255 usr = AttributeDict(usr.get_dict())
1256 usr.admin_row = True
1256 usr.admin_row = True
1257 usr.permission = _admin_perm
1257 usr.permission = _admin_perm
1258 super_admin_rows.append(usr)
1258 super_admin_rows.append(usr)
1259
1259
1260 return super_admin_rows + owner_row + perm_rows
1260 return super_admin_rows + owner_row + perm_rows
1261
1261
1262 def permission_user_groups(self):
1262 def permission_user_groups(self):
1263 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1263 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1264 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1264 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1265 joinedload(UserGroupUserGroupToPerm.target_user_group),
1265 joinedload(UserGroupUserGroupToPerm.target_user_group),
1266 joinedload(UserGroupUserGroupToPerm.permission),)
1266 joinedload(UserGroupUserGroupToPerm.permission),)
1267
1267
1268 perm_rows = []
1268 perm_rows = []
1269 for _user_group in q.all():
1269 for _user_group in q.all():
1270 usr = AttributeDict(_user_group.user_group.get_dict())
1270 usr = AttributeDict(_user_group.user_group.get_dict())
1271 usr.permission = _user_group.permission.permission_name
1271 usr.permission = _user_group.permission.permission_name
1272 perm_rows.append(usr)
1272 perm_rows.append(usr)
1273
1273
1274 return perm_rows
1274 return perm_rows
1275
1275
1276 def _get_default_perms(self, user_group, suffix=''):
1276 def _get_default_perms(self, user_group, suffix=''):
1277 from rhodecode.model.permission import PermissionModel
1277 from rhodecode.model.permission import PermissionModel
1278 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1278 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1279
1279
1280 def get_default_perms(self, suffix=''):
1280 def get_default_perms(self, suffix=''):
1281 return self._get_default_perms(self, suffix)
1281 return self._get_default_perms(self, suffix)
1282
1282
1283 def get_api_data(self, with_group_members=True, include_secrets=False):
1283 def get_api_data(self, with_group_members=True, include_secrets=False):
1284 """
1284 """
1285 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1285 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1286 basically forwarded.
1286 basically forwarded.
1287
1287
1288 """
1288 """
1289 user_group = self
1289 user_group = self
1290 data = {
1290 data = {
1291 'users_group_id': user_group.users_group_id,
1291 'users_group_id': user_group.users_group_id,
1292 'group_name': user_group.users_group_name,
1292 'group_name': user_group.users_group_name,
1293 'group_description': user_group.user_group_description,
1293 'group_description': user_group.user_group_description,
1294 'active': user_group.users_group_active,
1294 'active': user_group.users_group_active,
1295 'owner': user_group.user.username,
1295 'owner': user_group.user.username,
1296 'owner_email': user_group.user.email,
1296 'owner_email': user_group.user.email,
1297 }
1297 }
1298
1298
1299 if with_group_members:
1299 if with_group_members:
1300 users = []
1300 users = []
1301 for user in user_group.members:
1301 for user in user_group.members:
1302 user = user.user
1302 user = user.user
1303 users.append(user.get_api_data(include_secrets=include_secrets))
1303 users.append(user.get_api_data(include_secrets=include_secrets))
1304 data['users'] = users
1304 data['users'] = users
1305
1305
1306 return data
1306 return data
1307
1307
1308
1308
1309 class UserGroupMember(Base, BaseModel):
1309 class UserGroupMember(Base, BaseModel):
1310 __tablename__ = 'users_groups_members'
1310 __tablename__ = 'users_groups_members'
1311 __table_args__ = (
1311 __table_args__ = (
1312 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1312 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1313 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1313 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1314 )
1314 )
1315
1315
1316 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1316 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1317 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1317 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1318 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1318 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1319
1319
1320 user = relationship('User', lazy='joined')
1320 user = relationship('User', lazy='joined')
1321 users_group = relationship('UserGroup')
1321 users_group = relationship('UserGroup')
1322
1322
1323 def __init__(self, gr_id='', u_id=''):
1323 def __init__(self, gr_id='', u_id=''):
1324 self.users_group_id = gr_id
1324 self.users_group_id = gr_id
1325 self.user_id = u_id
1325 self.user_id = u_id
1326
1326
1327
1327
1328 class RepositoryField(Base, BaseModel):
1328 class RepositoryField(Base, BaseModel):
1329 __tablename__ = 'repositories_fields'
1329 __tablename__ = 'repositories_fields'
1330 __table_args__ = (
1330 __table_args__ = (
1331 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1331 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1332 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1332 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1333 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1333 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1334 )
1334 )
1335 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1335 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1336
1336
1337 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1337 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1338 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1338 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1339 field_key = Column("field_key", String(250))
1339 field_key = Column("field_key", String(250))
1340 field_label = Column("field_label", String(1024), nullable=False)
1340 field_label = Column("field_label", String(1024), nullable=False)
1341 field_value = Column("field_value", String(10000), nullable=False)
1341 field_value = Column("field_value", String(10000), nullable=False)
1342 field_desc = Column("field_desc", String(1024), nullable=False)
1342 field_desc = Column("field_desc", String(1024), nullable=False)
1343 field_type = Column("field_type", String(255), nullable=False, unique=None)
1343 field_type = Column("field_type", String(255), nullable=False, unique=None)
1344 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1344 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1345
1345
1346 repository = relationship('Repository')
1346 repository = relationship('Repository')
1347
1347
1348 @property
1348 @property
1349 def field_key_prefixed(self):
1349 def field_key_prefixed(self):
1350 return 'ex_%s' % self.field_key
1350 return 'ex_%s' % self.field_key
1351
1351
1352 @classmethod
1352 @classmethod
1353 def un_prefix_key(cls, key):
1353 def un_prefix_key(cls, key):
1354 if key.startswith(cls.PREFIX):
1354 if key.startswith(cls.PREFIX):
1355 return key[len(cls.PREFIX):]
1355 return key[len(cls.PREFIX):]
1356 return key
1356 return key
1357
1357
1358 @classmethod
1358 @classmethod
1359 def get_by_key_name(cls, key, repo):
1359 def get_by_key_name(cls, key, repo):
1360 row = cls.query()\
1360 row = cls.query()\
1361 .filter(cls.repository == repo)\
1361 .filter(cls.repository == repo)\
1362 .filter(cls.field_key == key).scalar()
1362 .filter(cls.field_key == key).scalar()
1363 return row
1363 return row
1364
1364
1365
1365
1366 class Repository(Base, BaseModel):
1366 class Repository(Base, BaseModel):
1367 __tablename__ = 'repositories'
1367 __tablename__ = 'repositories'
1368 __table_args__ = (
1368 __table_args__ = (
1369 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1369 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1370 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1370 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1371 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1371 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1372 )
1372 )
1373 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1373 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1374 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1374 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1375
1375
1376 STATE_CREATED = 'repo_state_created'
1376 STATE_CREATED = 'repo_state_created'
1377 STATE_PENDING = 'repo_state_pending'
1377 STATE_PENDING = 'repo_state_pending'
1378 STATE_ERROR = 'repo_state_error'
1378 STATE_ERROR = 'repo_state_error'
1379
1379
1380 LOCK_AUTOMATIC = 'lock_auto'
1380 LOCK_AUTOMATIC = 'lock_auto'
1381 LOCK_API = 'lock_api'
1381 LOCK_API = 'lock_api'
1382 LOCK_WEB = 'lock_web'
1382 LOCK_WEB = 'lock_web'
1383 LOCK_PULL = 'lock_pull'
1383 LOCK_PULL = 'lock_pull'
1384
1384
1385 NAME_SEP = URL_SEP
1385 NAME_SEP = URL_SEP
1386
1386
1387 repo_id = Column(
1387 repo_id = Column(
1388 "repo_id", Integer(), nullable=False, unique=True, default=None,
1388 "repo_id", Integer(), nullable=False, unique=True, default=None,
1389 primary_key=True)
1389 primary_key=True)
1390 _repo_name = Column(
1390 _repo_name = Column(
1391 "repo_name", Text(), nullable=False, default=None)
1391 "repo_name", Text(), nullable=False, default=None)
1392 _repo_name_hash = Column(
1392 _repo_name_hash = Column(
1393 "repo_name_hash", String(255), nullable=False, unique=True)
1393 "repo_name_hash", String(255), nullable=False, unique=True)
1394 repo_state = Column("repo_state", String(255), nullable=True)
1394 repo_state = Column("repo_state", String(255), nullable=True)
1395
1395
1396 clone_uri = Column(
1396 clone_uri = Column(
1397 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1397 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1398 default=None)
1398 default=None)
1399 repo_type = Column(
1399 repo_type = Column(
1400 "repo_type", String(255), nullable=False, unique=False, default=None)
1400 "repo_type", String(255), nullable=False, unique=False, default=None)
1401 user_id = Column(
1401 user_id = Column(
1402 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1402 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1403 unique=False, default=None)
1403 unique=False, default=None)
1404 private = Column(
1404 private = Column(
1405 "private", Boolean(), nullable=True, unique=None, default=None)
1405 "private", Boolean(), nullable=True, unique=None, default=None)
1406 enable_statistics = Column(
1406 enable_statistics = Column(
1407 "statistics", Boolean(), nullable=True, unique=None, default=True)
1407 "statistics", Boolean(), nullable=True, unique=None, default=True)
1408 enable_downloads = Column(
1408 enable_downloads = Column(
1409 "downloads", Boolean(), nullable=True, unique=None, default=True)
1409 "downloads", Boolean(), nullable=True, unique=None, default=True)
1410 description = Column(
1410 description = Column(
1411 "description", String(10000), nullable=True, unique=None, default=None)
1411 "description", String(10000), nullable=True, unique=None, default=None)
1412 created_on = Column(
1412 created_on = Column(
1413 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1413 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1414 default=datetime.datetime.now)
1414 default=datetime.datetime.now)
1415 updated_on = Column(
1415 updated_on = Column(
1416 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1416 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1417 default=datetime.datetime.now)
1417 default=datetime.datetime.now)
1418 _landing_revision = Column(
1418 _landing_revision = Column(
1419 "landing_revision", String(255), nullable=False, unique=False,
1419 "landing_revision", String(255), nullable=False, unique=False,
1420 default=None)
1420 default=None)
1421 enable_locking = Column(
1421 enable_locking = Column(
1422 "enable_locking", Boolean(), nullable=False, unique=None,
1422 "enable_locking", Boolean(), nullable=False, unique=None,
1423 default=False)
1423 default=False)
1424 _locked = Column(
1424 _locked = Column(
1425 "locked", String(255), nullable=True, unique=False, default=None)
1425 "locked", String(255), nullable=True, unique=False, default=None)
1426 _changeset_cache = Column(
1426 _changeset_cache = Column(
1427 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1427 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1428
1428
1429 fork_id = Column(
1429 fork_id = Column(
1430 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1430 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1431 nullable=True, unique=False, default=None)
1431 nullable=True, unique=False, default=None)
1432 group_id = Column(
1432 group_id = Column(
1433 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1433 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1434 unique=False, default=None)
1434 unique=False, default=None)
1435
1435
1436 user = relationship('User', lazy='joined')
1436 user = relationship('User', lazy='joined')
1437 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1437 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1438 group = relationship('RepoGroup', lazy='joined')
1438 group = relationship('RepoGroup', lazy='joined')
1439 repo_to_perm = relationship(
1439 repo_to_perm = relationship(
1440 'UserRepoToPerm', cascade='all',
1440 'UserRepoToPerm', cascade='all',
1441 order_by='UserRepoToPerm.repo_to_perm_id')
1441 order_by='UserRepoToPerm.repo_to_perm_id')
1442 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1442 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1443 stats = relationship('Statistics', cascade='all', uselist=False)
1443 stats = relationship('Statistics', cascade='all', uselist=False)
1444
1444
1445 followers = relationship(
1445 followers = relationship(
1446 'UserFollowing',
1446 'UserFollowing',
1447 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1447 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1448 cascade='all')
1448 cascade='all')
1449 extra_fields = relationship(
1449 extra_fields = relationship(
1450 'RepositoryField', cascade="all, delete, delete-orphan")
1450 'RepositoryField', cascade="all, delete, delete-orphan")
1451 logs = relationship('UserLog')
1451 logs = relationship('UserLog')
1452 comments = relationship(
1452 comments = relationship(
1453 'ChangesetComment', cascade="all, delete, delete-orphan")
1453 'ChangesetComment', cascade="all, delete, delete-orphan")
1454 pull_requests_source = relationship(
1454 pull_requests_source = relationship(
1455 'PullRequest',
1455 'PullRequest',
1456 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1456 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1457 cascade="all, delete, delete-orphan")
1457 cascade="all, delete, delete-orphan")
1458 pull_requests_target = relationship(
1458 pull_requests_target = relationship(
1459 'PullRequest',
1459 'PullRequest',
1460 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1460 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1461 cascade="all, delete, delete-orphan")
1461 cascade="all, delete, delete-orphan")
1462 ui = relationship('RepoRhodeCodeUi', cascade="all")
1462 ui = relationship('RepoRhodeCodeUi', cascade="all")
1463 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1463 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1464 integrations = relationship('Integration',
1464 integrations = relationship('Integration',
1465 cascade="all, delete, delete-orphan")
1465 cascade="all, delete, delete-orphan")
1466
1466
1467 def __unicode__(self):
1467 def __unicode__(self):
1468 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1468 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1469 safe_unicode(self.repo_name))
1469 safe_unicode(self.repo_name))
1470
1470
1471 @hybrid_property
1471 @hybrid_property
1472 def landing_rev(self):
1472 def landing_rev(self):
1473 # always should return [rev_type, rev]
1473 # always should return [rev_type, rev]
1474 if self._landing_revision:
1474 if self._landing_revision:
1475 _rev_info = self._landing_revision.split(':')
1475 _rev_info = self._landing_revision.split(':')
1476 if len(_rev_info) < 2:
1476 if len(_rev_info) < 2:
1477 _rev_info.insert(0, 'rev')
1477 _rev_info.insert(0, 'rev')
1478 return [_rev_info[0], _rev_info[1]]
1478 return [_rev_info[0], _rev_info[1]]
1479 return [None, None]
1479 return [None, None]
1480
1480
1481 @landing_rev.setter
1481 @landing_rev.setter
1482 def landing_rev(self, val):
1482 def landing_rev(self, val):
1483 if ':' not in val:
1483 if ':' not in val:
1484 raise ValueError('value must be delimited with `:` and consist '
1484 raise ValueError('value must be delimited with `:` and consist '
1485 'of <rev_type>:<rev>, got %s instead' % val)
1485 'of <rev_type>:<rev>, got %s instead' % val)
1486 self._landing_revision = val
1486 self._landing_revision = val
1487
1487
1488 @hybrid_property
1488 @hybrid_property
1489 def locked(self):
1489 def locked(self):
1490 if self._locked:
1490 if self._locked:
1491 user_id, timelocked, reason = self._locked.split(':')
1491 user_id, timelocked, reason = self._locked.split(':')
1492 lock_values = int(user_id), timelocked, reason
1492 lock_values = int(user_id), timelocked, reason
1493 else:
1493 else:
1494 lock_values = [None, None, None]
1494 lock_values = [None, None, None]
1495 return lock_values
1495 return lock_values
1496
1496
1497 @locked.setter
1497 @locked.setter
1498 def locked(self, val):
1498 def locked(self, val):
1499 if val and isinstance(val, (list, tuple)):
1499 if val and isinstance(val, (list, tuple)):
1500 self._locked = ':'.join(map(str, val))
1500 self._locked = ':'.join(map(str, val))
1501 else:
1501 else:
1502 self._locked = None
1502 self._locked = None
1503
1503
1504 @hybrid_property
1504 @hybrid_property
1505 def changeset_cache(self):
1505 def changeset_cache(self):
1506 from rhodecode.lib.vcs.backends.base import EmptyCommit
1506 from rhodecode.lib.vcs.backends.base import EmptyCommit
1507 dummy = EmptyCommit().__json__()
1507 dummy = EmptyCommit().__json__()
1508 if not self._changeset_cache:
1508 if not self._changeset_cache:
1509 return dummy
1509 return dummy
1510 try:
1510 try:
1511 return json.loads(self._changeset_cache)
1511 return json.loads(self._changeset_cache)
1512 except TypeError:
1512 except TypeError:
1513 return dummy
1513 return dummy
1514 except Exception:
1514 except Exception:
1515 log.error(traceback.format_exc())
1515 log.error(traceback.format_exc())
1516 return dummy
1516 return dummy
1517
1517
1518 @changeset_cache.setter
1518 @changeset_cache.setter
1519 def changeset_cache(self, val):
1519 def changeset_cache(self, val):
1520 try:
1520 try:
1521 self._changeset_cache = json.dumps(val)
1521 self._changeset_cache = json.dumps(val)
1522 except Exception:
1522 except Exception:
1523 log.error(traceback.format_exc())
1523 log.error(traceback.format_exc())
1524
1524
1525 @hybrid_property
1525 @hybrid_property
1526 def repo_name(self):
1526 def repo_name(self):
1527 return self._repo_name
1527 return self._repo_name
1528
1528
1529 @repo_name.setter
1529 @repo_name.setter
1530 def repo_name(self, value):
1530 def repo_name(self, value):
1531 self._repo_name = value
1531 self._repo_name = value
1532 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1532 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1533
1533
1534 @classmethod
1534 @classmethod
1535 def normalize_repo_name(cls, repo_name):
1535 def normalize_repo_name(cls, repo_name):
1536 """
1536 """
1537 Normalizes os specific repo_name to the format internally stored inside
1537 Normalizes os specific repo_name to the format internally stored inside
1538 database using URL_SEP
1538 database using URL_SEP
1539
1539
1540 :param cls:
1540 :param cls:
1541 :param repo_name:
1541 :param repo_name:
1542 """
1542 """
1543 return cls.NAME_SEP.join(repo_name.split(os.sep))
1543 return cls.NAME_SEP.join(repo_name.split(os.sep))
1544
1544
1545 @classmethod
1545 @classmethod
1546 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1546 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1547 session = Session()
1547 session = Session()
1548 q = session.query(cls).filter(cls.repo_name == repo_name)
1548 q = session.query(cls).filter(cls.repo_name == repo_name)
1549
1549
1550 if cache:
1550 if cache:
1551 if identity_cache:
1551 if identity_cache:
1552 val = cls.identity_cache(session, 'repo_name', repo_name)
1552 val = cls.identity_cache(session, 'repo_name', repo_name)
1553 if val:
1553 if val:
1554 return val
1554 return val
1555 else:
1555 else:
1556 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1556 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1557 q = q.options(
1557 q = q.options(
1558 FromCache("sql_cache_short", cache_key))
1558 FromCache("sql_cache_short", cache_key))
1559
1559
1560 return q.scalar()
1560 return q.scalar()
1561
1561
1562 @classmethod
1562 @classmethod
1563 def get_by_full_path(cls, repo_full_path):
1563 def get_by_full_path(cls, repo_full_path):
1564 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1564 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1565 repo_name = cls.normalize_repo_name(repo_name)
1565 repo_name = cls.normalize_repo_name(repo_name)
1566 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1566 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1567
1567
1568 @classmethod
1568 @classmethod
1569 def get_repo_forks(cls, repo_id):
1569 def get_repo_forks(cls, repo_id):
1570 return cls.query().filter(Repository.fork_id == repo_id)
1570 return cls.query().filter(Repository.fork_id == repo_id)
1571
1571
1572 @classmethod
1572 @classmethod
1573 def base_path(cls):
1573 def base_path(cls):
1574 """
1574 """
1575 Returns base path when all repos are stored
1575 Returns base path when all repos are stored
1576
1576
1577 :param cls:
1577 :param cls:
1578 """
1578 """
1579 q = Session().query(RhodeCodeUi)\
1579 q = Session().query(RhodeCodeUi)\
1580 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1580 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1581 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1581 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1582 return q.one().ui_value
1582 return q.one().ui_value
1583
1583
1584 @classmethod
1584 @classmethod
1585 def is_valid(cls, repo_name):
1585 def is_valid(cls, repo_name):
1586 """
1586 """
1587 returns True if given repo name is a valid filesystem repository
1587 returns True if given repo name is a valid filesystem repository
1588
1588
1589 :param cls:
1589 :param cls:
1590 :param repo_name:
1590 :param repo_name:
1591 """
1591 """
1592 from rhodecode.lib.utils import is_valid_repo
1592 from rhodecode.lib.utils import is_valid_repo
1593
1593
1594 return is_valid_repo(repo_name, cls.base_path())
1594 return is_valid_repo(repo_name, cls.base_path())
1595
1595
1596 @classmethod
1596 @classmethod
1597 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1597 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1598 case_insensitive=True):
1598 case_insensitive=True):
1599 q = Repository.query()
1599 q = Repository.query()
1600
1600
1601 if not isinstance(user_id, Optional):
1601 if not isinstance(user_id, Optional):
1602 q = q.filter(Repository.user_id == user_id)
1602 q = q.filter(Repository.user_id == user_id)
1603
1603
1604 if not isinstance(group_id, Optional):
1604 if not isinstance(group_id, Optional):
1605 q = q.filter(Repository.group_id == group_id)
1605 q = q.filter(Repository.group_id == group_id)
1606
1606
1607 if case_insensitive:
1607 if case_insensitive:
1608 q = q.order_by(func.lower(Repository.repo_name))
1608 q = q.order_by(func.lower(Repository.repo_name))
1609 else:
1609 else:
1610 q = q.order_by(Repository.repo_name)
1610 q = q.order_by(Repository.repo_name)
1611 return q.all()
1611 return q.all()
1612
1612
1613 @property
1613 @property
1614 def forks(self):
1614 def forks(self):
1615 """
1615 """
1616 Return forks of this repo
1616 Return forks of this repo
1617 """
1617 """
1618 return Repository.get_repo_forks(self.repo_id)
1618 return Repository.get_repo_forks(self.repo_id)
1619
1619
1620 @property
1620 @property
1621 def parent(self):
1621 def parent(self):
1622 """
1622 """
1623 Returns fork parent
1623 Returns fork parent
1624 """
1624 """
1625 return self.fork
1625 return self.fork
1626
1626
1627 @property
1627 @property
1628 def just_name(self):
1628 def just_name(self):
1629 return self.repo_name.split(self.NAME_SEP)[-1]
1629 return self.repo_name.split(self.NAME_SEP)[-1]
1630
1630
1631 @property
1631 @property
1632 def groups_with_parents(self):
1632 def groups_with_parents(self):
1633 groups = []
1633 groups = []
1634 if self.group is None:
1634 if self.group is None:
1635 return groups
1635 return groups
1636
1636
1637 cur_gr = self.group
1637 cur_gr = self.group
1638 groups.insert(0, cur_gr)
1638 groups.insert(0, cur_gr)
1639 while 1:
1639 while 1:
1640 gr = getattr(cur_gr, 'parent_group', None)
1640 gr = getattr(cur_gr, 'parent_group', None)
1641 cur_gr = cur_gr.parent_group
1641 cur_gr = cur_gr.parent_group
1642 if gr is None:
1642 if gr is None:
1643 break
1643 break
1644 groups.insert(0, gr)
1644 groups.insert(0, gr)
1645
1645
1646 return groups
1646 return groups
1647
1647
1648 @property
1648 @property
1649 def groups_and_repo(self):
1649 def groups_and_repo(self):
1650 return self.groups_with_parents, self
1650 return self.groups_with_parents, self
1651
1651
1652 @LazyProperty
1652 @LazyProperty
1653 def repo_path(self):
1653 def repo_path(self):
1654 """
1654 """
1655 Returns base full path for that repository means where it actually
1655 Returns base full path for that repository means where it actually
1656 exists on a filesystem
1656 exists on a filesystem
1657 """
1657 """
1658 q = Session().query(RhodeCodeUi).filter(
1658 q = Session().query(RhodeCodeUi).filter(
1659 RhodeCodeUi.ui_key == self.NAME_SEP)
1659 RhodeCodeUi.ui_key == self.NAME_SEP)
1660 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1660 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1661 return q.one().ui_value
1661 return q.one().ui_value
1662
1662
1663 @property
1663 @property
1664 def repo_full_path(self):
1664 def repo_full_path(self):
1665 p = [self.repo_path]
1665 p = [self.repo_path]
1666 # we need to split the name by / since this is how we store the
1666 # we need to split the name by / since this is how we store the
1667 # names in the database, but that eventually needs to be converted
1667 # names in the database, but that eventually needs to be converted
1668 # into a valid system path
1668 # into a valid system path
1669 p += self.repo_name.split(self.NAME_SEP)
1669 p += self.repo_name.split(self.NAME_SEP)
1670 return os.path.join(*map(safe_unicode, p))
1670 return os.path.join(*map(safe_unicode, p))
1671
1671
1672 @property
1672 @property
1673 def cache_keys(self):
1673 def cache_keys(self):
1674 """
1674 """
1675 Returns associated cache keys for that repo
1675 Returns associated cache keys for that repo
1676 """
1676 """
1677 return CacheKey.query()\
1677 return CacheKey.query()\
1678 .filter(CacheKey.cache_args == self.repo_name)\
1678 .filter(CacheKey.cache_args == self.repo_name)\
1679 .order_by(CacheKey.cache_key)\
1679 .order_by(CacheKey.cache_key)\
1680 .all()
1680 .all()
1681
1681
1682 def get_new_name(self, repo_name):
1682 def get_new_name(self, repo_name):
1683 """
1683 """
1684 returns new full repository name based on assigned group and new new
1684 returns new full repository name based on assigned group and new new
1685
1685
1686 :param group_name:
1686 :param group_name:
1687 """
1687 """
1688 path_prefix = self.group.full_path_splitted if self.group else []
1688 path_prefix = self.group.full_path_splitted if self.group else []
1689 return self.NAME_SEP.join(path_prefix + [repo_name])
1689 return self.NAME_SEP.join(path_prefix + [repo_name])
1690
1690
1691 @property
1691 @property
1692 def _config(self):
1692 def _config(self):
1693 """
1693 """
1694 Returns db based config object.
1694 Returns db based config object.
1695 """
1695 """
1696 from rhodecode.lib.utils import make_db_config
1696 from rhodecode.lib.utils import make_db_config
1697 return make_db_config(clear_session=False, repo=self)
1697 return make_db_config(clear_session=False, repo=self)
1698
1698
1699 def permissions(self, with_admins=True, with_owner=True):
1699 def permissions(self, with_admins=True, with_owner=True):
1700 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1700 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1701 q = q.options(joinedload(UserRepoToPerm.repository),
1701 q = q.options(joinedload(UserRepoToPerm.repository),
1702 joinedload(UserRepoToPerm.user),
1702 joinedload(UserRepoToPerm.user),
1703 joinedload(UserRepoToPerm.permission),)
1703 joinedload(UserRepoToPerm.permission),)
1704
1704
1705 # get owners and admins and permissions. We do a trick of re-writing
1705 # get owners and admins and permissions. We do a trick of re-writing
1706 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1706 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1707 # has a global reference and changing one object propagates to all
1707 # has a global reference and changing one object propagates to all
1708 # others. This means if admin is also an owner admin_row that change
1708 # others. This means if admin is also an owner admin_row that change
1709 # would propagate to both objects
1709 # would propagate to both objects
1710 perm_rows = []
1710 perm_rows = []
1711 for _usr in q.all():
1711 for _usr in q.all():
1712 usr = AttributeDict(_usr.user.get_dict())
1712 usr = AttributeDict(_usr.user.get_dict())
1713 usr.permission = _usr.permission.permission_name
1713 usr.permission = _usr.permission.permission_name
1714 perm_rows.append(usr)
1714 perm_rows.append(usr)
1715
1715
1716 # filter the perm rows by 'default' first and then sort them by
1716 # filter the perm rows by 'default' first and then sort them by
1717 # admin,write,read,none permissions sorted again alphabetically in
1717 # admin,write,read,none permissions sorted again alphabetically in
1718 # each group
1718 # each group
1719 perm_rows = sorted(perm_rows, key=display_sort)
1719 perm_rows = sorted(perm_rows, key=display_sort)
1720
1720
1721 _admin_perm = 'repository.admin'
1721 _admin_perm = 'repository.admin'
1722 owner_row = []
1722 owner_row = []
1723 if with_owner:
1723 if with_owner:
1724 usr = AttributeDict(self.user.get_dict())
1724 usr = AttributeDict(self.user.get_dict())
1725 usr.owner_row = True
1725 usr.owner_row = True
1726 usr.permission = _admin_perm
1726 usr.permission = _admin_perm
1727 owner_row.append(usr)
1727 owner_row.append(usr)
1728
1728
1729 super_admin_rows = []
1729 super_admin_rows = []
1730 if with_admins:
1730 if with_admins:
1731 for usr in User.get_all_super_admins():
1731 for usr in User.get_all_super_admins():
1732 # if this admin is also owner, don't double the record
1732 # if this admin is also owner, don't double the record
1733 if usr.user_id == owner_row[0].user_id:
1733 if usr.user_id == owner_row[0].user_id:
1734 owner_row[0].admin_row = True
1734 owner_row[0].admin_row = True
1735 else:
1735 else:
1736 usr = AttributeDict(usr.get_dict())
1736 usr = AttributeDict(usr.get_dict())
1737 usr.admin_row = True
1737 usr.admin_row = True
1738 usr.permission = _admin_perm
1738 usr.permission = _admin_perm
1739 super_admin_rows.append(usr)
1739 super_admin_rows.append(usr)
1740
1740
1741 return super_admin_rows + owner_row + perm_rows
1741 return super_admin_rows + owner_row + perm_rows
1742
1742
1743 def permission_user_groups(self):
1743 def permission_user_groups(self):
1744 q = UserGroupRepoToPerm.query().filter(
1744 q = UserGroupRepoToPerm.query().filter(
1745 UserGroupRepoToPerm.repository == self)
1745 UserGroupRepoToPerm.repository == self)
1746 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1746 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1747 joinedload(UserGroupRepoToPerm.users_group),
1747 joinedload(UserGroupRepoToPerm.users_group),
1748 joinedload(UserGroupRepoToPerm.permission),)
1748 joinedload(UserGroupRepoToPerm.permission),)
1749
1749
1750 perm_rows = []
1750 perm_rows = []
1751 for _user_group in q.all():
1751 for _user_group in q.all():
1752 usr = AttributeDict(_user_group.users_group.get_dict())
1752 usr = AttributeDict(_user_group.users_group.get_dict())
1753 usr.permission = _user_group.permission.permission_name
1753 usr.permission = _user_group.permission.permission_name
1754 perm_rows.append(usr)
1754 perm_rows.append(usr)
1755
1755
1756 return perm_rows
1756 return perm_rows
1757
1757
1758 def get_api_data(self, include_secrets=False):
1758 def get_api_data(self, include_secrets=False):
1759 """
1759 """
1760 Common function for generating repo api data
1760 Common function for generating repo api data
1761
1761
1762 :param include_secrets: See :meth:`User.get_api_data`.
1762 :param include_secrets: See :meth:`User.get_api_data`.
1763
1763
1764 """
1764 """
1765 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1765 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1766 # move this methods on models level.
1766 # move this methods on models level.
1767 from rhodecode.model.settings import SettingsModel
1767 from rhodecode.model.settings import SettingsModel
1768 from rhodecode.model.repo import RepoModel
1768 from rhodecode.model.repo import RepoModel
1769
1769
1770 repo = self
1770 repo = self
1771 _user_id, _time, _reason = self.locked
1771 _user_id, _time, _reason = self.locked
1772
1772
1773 data = {
1773 data = {
1774 'repo_id': repo.repo_id,
1774 'repo_id': repo.repo_id,
1775 'repo_name': repo.repo_name,
1775 'repo_name': repo.repo_name,
1776 'repo_type': repo.repo_type,
1776 'repo_type': repo.repo_type,
1777 'clone_uri': repo.clone_uri or '',
1777 'clone_uri': repo.clone_uri or '',
1778 'url': RepoModel().get_url(self),
1778 'url': RepoModel().get_url(self),
1779 'private': repo.private,
1779 'private': repo.private,
1780 'created_on': repo.created_on,
1780 'created_on': repo.created_on,
1781 'description': repo.description,
1781 'description': repo.description,
1782 'landing_rev': repo.landing_rev,
1782 'landing_rev': repo.landing_rev,
1783 'owner': repo.user.username,
1783 'owner': repo.user.username,
1784 'fork_of': repo.fork.repo_name if repo.fork else None,
1784 'fork_of': repo.fork.repo_name if repo.fork else None,
1785 'enable_statistics': repo.enable_statistics,
1785 'enable_statistics': repo.enable_statistics,
1786 'enable_locking': repo.enable_locking,
1786 'enable_locking': repo.enable_locking,
1787 'enable_downloads': repo.enable_downloads,
1787 'enable_downloads': repo.enable_downloads,
1788 'last_changeset': repo.changeset_cache,
1788 'last_changeset': repo.changeset_cache,
1789 'locked_by': User.get(_user_id).get_api_data(
1789 'locked_by': User.get(_user_id).get_api_data(
1790 include_secrets=include_secrets) if _user_id else None,
1790 include_secrets=include_secrets) if _user_id else None,
1791 'locked_date': time_to_datetime(_time) if _time else None,
1791 'locked_date': time_to_datetime(_time) if _time else None,
1792 'lock_reason': _reason if _reason else None,
1792 'lock_reason': _reason if _reason else None,
1793 }
1793 }
1794
1794
1795 # TODO: mikhail: should be per-repo settings here
1795 # TODO: mikhail: should be per-repo settings here
1796 rc_config = SettingsModel().get_all_settings()
1796 rc_config = SettingsModel().get_all_settings()
1797 repository_fields = str2bool(
1797 repository_fields = str2bool(
1798 rc_config.get('rhodecode_repository_fields'))
1798 rc_config.get('rhodecode_repository_fields'))
1799 if repository_fields:
1799 if repository_fields:
1800 for f in self.extra_fields:
1800 for f in self.extra_fields:
1801 data[f.field_key_prefixed] = f.field_value
1801 data[f.field_key_prefixed] = f.field_value
1802
1802
1803 return data
1803 return data
1804
1804
1805 @classmethod
1805 @classmethod
1806 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1806 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1807 if not lock_time:
1807 if not lock_time:
1808 lock_time = time.time()
1808 lock_time = time.time()
1809 if not lock_reason:
1809 if not lock_reason:
1810 lock_reason = cls.LOCK_AUTOMATIC
1810 lock_reason = cls.LOCK_AUTOMATIC
1811 repo.locked = [user_id, lock_time, lock_reason]
1811 repo.locked = [user_id, lock_time, lock_reason]
1812 Session().add(repo)
1812 Session().add(repo)
1813 Session().commit()
1813 Session().commit()
1814
1814
1815 @classmethod
1815 @classmethod
1816 def unlock(cls, repo):
1816 def unlock(cls, repo):
1817 repo.locked = None
1817 repo.locked = None
1818 Session().add(repo)
1818 Session().add(repo)
1819 Session().commit()
1819 Session().commit()
1820
1820
1821 @classmethod
1821 @classmethod
1822 def getlock(cls, repo):
1822 def getlock(cls, repo):
1823 return repo.locked
1823 return repo.locked
1824
1824
1825 def is_user_lock(self, user_id):
1825 def is_user_lock(self, user_id):
1826 if self.lock[0]:
1826 if self.lock[0]:
1827 lock_user_id = safe_int(self.lock[0])
1827 lock_user_id = safe_int(self.lock[0])
1828 user_id = safe_int(user_id)
1828 user_id = safe_int(user_id)
1829 # both are ints, and they are equal
1829 # both are ints, and they are equal
1830 return all([lock_user_id, user_id]) and lock_user_id == user_id
1830 return all([lock_user_id, user_id]) and lock_user_id == user_id
1831
1831
1832 return False
1832 return False
1833
1833
1834 def get_locking_state(self, action, user_id, only_when_enabled=True):
1834 def get_locking_state(self, action, user_id, only_when_enabled=True):
1835 """
1835 """
1836 Checks locking on this repository, if locking is enabled and lock is
1836 Checks locking on this repository, if locking is enabled and lock is
1837 present returns a tuple of make_lock, locked, locked_by.
1837 present returns a tuple of make_lock, locked, locked_by.
1838 make_lock can have 3 states None (do nothing) True, make lock
1838 make_lock can have 3 states None (do nothing) True, make lock
1839 False release lock, This value is later propagated to hooks, which
1839 False release lock, This value is later propagated to hooks, which
1840 do the locking. Think about this as signals passed to hooks what to do.
1840 do the locking. Think about this as signals passed to hooks what to do.
1841
1841
1842 """
1842 """
1843 # TODO: johbo: This is part of the business logic and should be moved
1843 # TODO: johbo: This is part of the business logic and should be moved
1844 # into the RepositoryModel.
1844 # into the RepositoryModel.
1845
1845
1846 if action not in ('push', 'pull'):
1846 if action not in ('push', 'pull'):
1847 raise ValueError("Invalid action value: %s" % repr(action))
1847 raise ValueError("Invalid action value: %s" % repr(action))
1848
1848
1849 # defines if locked error should be thrown to user
1849 # defines if locked error should be thrown to user
1850 currently_locked = False
1850 currently_locked = False
1851 # defines if new lock should be made, tri-state
1851 # defines if new lock should be made, tri-state
1852 make_lock = None
1852 make_lock = None
1853 repo = self
1853 repo = self
1854 user = User.get(user_id)
1854 user = User.get(user_id)
1855
1855
1856 lock_info = repo.locked
1856 lock_info = repo.locked
1857
1857
1858 if repo and (repo.enable_locking or not only_when_enabled):
1858 if repo and (repo.enable_locking or not only_when_enabled):
1859 if action == 'push':
1859 if action == 'push':
1860 # check if it's already locked !, if it is compare users
1860 # check if it's already locked !, if it is compare users
1861 locked_by_user_id = lock_info[0]
1861 locked_by_user_id = lock_info[0]
1862 if user.user_id == locked_by_user_id:
1862 if user.user_id == locked_by_user_id:
1863 log.debug(
1863 log.debug(
1864 'Got `push` action from user %s, now unlocking', user)
1864 'Got `push` action from user %s, now unlocking', user)
1865 # unlock if we have push from user who locked
1865 # unlock if we have push from user who locked
1866 make_lock = False
1866 make_lock = False
1867 else:
1867 else:
1868 # we're not the same user who locked, ban with
1868 # we're not the same user who locked, ban with
1869 # code defined in settings (default is 423 HTTP Locked) !
1869 # code defined in settings (default is 423 HTTP Locked) !
1870 log.debug('Repo %s is currently locked by %s', repo, user)
1870 log.debug('Repo %s is currently locked by %s', repo, user)
1871 currently_locked = True
1871 currently_locked = True
1872 elif action == 'pull':
1872 elif action == 'pull':
1873 # [0] user [1] date
1873 # [0] user [1] date
1874 if lock_info[0] and lock_info[1]:
1874 if lock_info[0] and lock_info[1]:
1875 log.debug('Repo %s is currently locked by %s', repo, user)
1875 log.debug('Repo %s is currently locked by %s', repo, user)
1876 currently_locked = True
1876 currently_locked = True
1877 else:
1877 else:
1878 log.debug('Setting lock on repo %s by %s', repo, user)
1878 log.debug('Setting lock on repo %s by %s', repo, user)
1879 make_lock = True
1879 make_lock = True
1880
1880
1881 else:
1881 else:
1882 log.debug('Repository %s do not have locking enabled', repo)
1882 log.debug('Repository %s do not have locking enabled', repo)
1883
1883
1884 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1884 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1885 make_lock, currently_locked, lock_info)
1885 make_lock, currently_locked, lock_info)
1886
1886
1887 from rhodecode.lib.auth import HasRepoPermissionAny
1887 from rhodecode.lib.auth import HasRepoPermissionAny
1888 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1888 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1889 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1889 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1890 # if we don't have at least write permission we cannot make a lock
1890 # if we don't have at least write permission we cannot make a lock
1891 log.debug('lock state reset back to FALSE due to lack '
1891 log.debug('lock state reset back to FALSE due to lack '
1892 'of at least read permission')
1892 'of at least read permission')
1893 make_lock = False
1893 make_lock = False
1894
1894
1895 return make_lock, currently_locked, lock_info
1895 return make_lock, currently_locked, lock_info
1896
1896
1897 @property
1897 @property
1898 def last_db_change(self):
1898 def last_db_change(self):
1899 return self.updated_on
1899 return self.updated_on
1900
1900
1901 @property
1901 @property
1902 def clone_uri_hidden(self):
1902 def clone_uri_hidden(self):
1903 clone_uri = self.clone_uri
1903 clone_uri = self.clone_uri
1904 if clone_uri:
1904 if clone_uri:
1905 import urlobject
1905 import urlobject
1906 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1906 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1907 if url_obj.password:
1907 if url_obj.password:
1908 clone_uri = url_obj.with_password('*****')
1908 clone_uri = url_obj.with_password('*****')
1909 return clone_uri
1909 return clone_uri
1910
1910
1911 def clone_url(self, **override):
1911 def clone_url(self, **override):
1912
1912
1913 uri_tmpl = None
1913 uri_tmpl = None
1914 if 'with_id' in override:
1914 if 'with_id' in override:
1915 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1915 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1916 del override['with_id']
1916 del override['with_id']
1917
1917
1918 if 'uri_tmpl' in override:
1918 if 'uri_tmpl' in override:
1919 uri_tmpl = override['uri_tmpl']
1919 uri_tmpl = override['uri_tmpl']
1920 del override['uri_tmpl']
1920 del override['uri_tmpl']
1921
1921
1922 # we didn't override our tmpl from **overrides
1922 # we didn't override our tmpl from **overrides
1923 if not uri_tmpl:
1923 if not uri_tmpl:
1924 uri_tmpl = self.DEFAULT_CLONE_URI
1924 uri_tmpl = self.DEFAULT_CLONE_URI
1925 try:
1925 try:
1926 from pylons import tmpl_context as c
1926 from pylons import tmpl_context as c
1927 uri_tmpl = c.clone_uri_tmpl
1927 uri_tmpl = c.clone_uri_tmpl
1928 except Exception:
1928 except Exception:
1929 # in any case if we call this outside of request context,
1929 # in any case if we call this outside of request context,
1930 # ie, not having tmpl_context set up
1930 # ie, not having tmpl_context set up
1931 pass
1931 pass
1932
1932
1933 request = get_current_request()
1933 request = get_current_request()
1934 return get_clone_url(request=request,
1934 return get_clone_url(request=request,
1935 uri_tmpl=uri_tmpl,
1935 uri_tmpl=uri_tmpl,
1936 repo_name=self.repo_name,
1936 repo_name=self.repo_name,
1937 repo_id=self.repo_id, **override)
1937 repo_id=self.repo_id, **override)
1938
1938
1939 def set_state(self, state):
1939 def set_state(self, state):
1940 self.repo_state = state
1940 self.repo_state = state
1941 Session().add(self)
1941 Session().add(self)
1942 #==========================================================================
1942 #==========================================================================
1943 # SCM PROPERTIES
1943 # SCM PROPERTIES
1944 #==========================================================================
1944 #==========================================================================
1945
1945
1946 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1946 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1947 return get_commit_safe(
1947 return get_commit_safe(
1948 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1948 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1949
1949
1950 def get_changeset(self, rev=None, pre_load=None):
1950 def get_changeset(self, rev=None, pre_load=None):
1951 warnings.warn("Use get_commit", DeprecationWarning)
1951 warnings.warn("Use get_commit", DeprecationWarning)
1952 commit_id = None
1952 commit_id = None
1953 commit_idx = None
1953 commit_idx = None
1954 if isinstance(rev, basestring):
1954 if isinstance(rev, basestring):
1955 commit_id = rev
1955 commit_id = rev
1956 else:
1956 else:
1957 commit_idx = rev
1957 commit_idx = rev
1958 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1958 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1959 pre_load=pre_load)
1959 pre_load=pre_load)
1960
1960
1961 def get_landing_commit(self):
1961 def get_landing_commit(self):
1962 """
1962 """
1963 Returns landing commit, or if that doesn't exist returns the tip
1963 Returns landing commit, or if that doesn't exist returns the tip
1964 """
1964 """
1965 _rev_type, _rev = self.landing_rev
1965 _rev_type, _rev = self.landing_rev
1966 commit = self.get_commit(_rev)
1966 commit = self.get_commit(_rev)
1967 if isinstance(commit, EmptyCommit):
1967 if isinstance(commit, EmptyCommit):
1968 return self.get_commit()
1968 return self.get_commit()
1969 return commit
1969 return commit
1970
1970
1971 def update_commit_cache(self, cs_cache=None, config=None):
1971 def update_commit_cache(self, cs_cache=None, config=None):
1972 """
1972 """
1973 Update cache of last changeset for repository, keys should be::
1973 Update cache of last changeset for repository, keys should be::
1974
1974
1975 short_id
1975 short_id
1976 raw_id
1976 raw_id
1977 revision
1977 revision
1978 parents
1978 parents
1979 message
1979 message
1980 date
1980 date
1981 author
1981 author
1982
1982
1983 :param cs_cache:
1983 :param cs_cache:
1984 """
1984 """
1985 from rhodecode.lib.vcs.backends.base import BaseChangeset
1985 from rhodecode.lib.vcs.backends.base import BaseChangeset
1986 if cs_cache is None:
1986 if cs_cache is None:
1987 # use no-cache version here
1987 # use no-cache version here
1988 scm_repo = self.scm_instance(cache=False, config=config)
1988 scm_repo = self.scm_instance(cache=False, config=config)
1989 if scm_repo:
1989 if scm_repo:
1990 cs_cache = scm_repo.get_commit(
1990 cs_cache = scm_repo.get_commit(
1991 pre_load=["author", "date", "message", "parents"])
1991 pre_load=["author", "date", "message", "parents"])
1992 else:
1992 else:
1993 cs_cache = EmptyCommit()
1993 cs_cache = EmptyCommit()
1994
1994
1995 if isinstance(cs_cache, BaseChangeset):
1995 if isinstance(cs_cache, BaseChangeset):
1996 cs_cache = cs_cache.__json__()
1996 cs_cache = cs_cache.__json__()
1997
1997
1998 def is_outdated(new_cs_cache):
1998 def is_outdated(new_cs_cache):
1999 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1999 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2000 new_cs_cache['revision'] != self.changeset_cache['revision']):
2000 new_cs_cache['revision'] != self.changeset_cache['revision']):
2001 return True
2001 return True
2002 return False
2002 return False
2003
2003
2004 # check if we have maybe already latest cached revision
2004 # check if we have maybe already latest cached revision
2005 if is_outdated(cs_cache) or not self.changeset_cache:
2005 if is_outdated(cs_cache) or not self.changeset_cache:
2006 _default = datetime.datetime.fromtimestamp(0)
2006 _default = datetime.datetime.fromtimestamp(0)
2007 last_change = cs_cache.get('date') or _default
2007 last_change = cs_cache.get('date') or _default
2008 log.debug('updated repo %s with new cs cache %s',
2008 log.debug('updated repo %s with new cs cache %s',
2009 self.repo_name, cs_cache)
2009 self.repo_name, cs_cache)
2010 self.updated_on = last_change
2010 self.updated_on = last_change
2011 self.changeset_cache = cs_cache
2011 self.changeset_cache = cs_cache
2012 Session().add(self)
2012 Session().add(self)
2013 Session().commit()
2013 Session().commit()
2014 else:
2014 else:
2015 log.debug('Skipping update_commit_cache for repo:`%s` '
2015 log.debug('Skipping update_commit_cache for repo:`%s` '
2016 'commit already with latest changes', self.repo_name)
2016 'commit already with latest changes', self.repo_name)
2017
2017
2018 @property
2018 @property
2019 def tip(self):
2019 def tip(self):
2020 return self.get_commit('tip')
2020 return self.get_commit('tip')
2021
2021
2022 @property
2022 @property
2023 def author(self):
2023 def author(self):
2024 return self.tip.author
2024 return self.tip.author
2025
2025
2026 @property
2026 @property
2027 def last_change(self):
2027 def last_change(self):
2028 return self.scm_instance().last_change
2028 return self.scm_instance().last_change
2029
2029
2030 def get_comments(self, revisions=None):
2030 def get_comments(self, revisions=None):
2031 """
2031 """
2032 Returns comments for this repository grouped by revisions
2032 Returns comments for this repository grouped by revisions
2033
2033
2034 :param revisions: filter query by revisions only
2034 :param revisions: filter query by revisions only
2035 """
2035 """
2036 cmts = ChangesetComment.query()\
2036 cmts = ChangesetComment.query()\
2037 .filter(ChangesetComment.repo == self)
2037 .filter(ChangesetComment.repo == self)
2038 if revisions:
2038 if revisions:
2039 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2039 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2040 grouped = collections.defaultdict(list)
2040 grouped = collections.defaultdict(list)
2041 for cmt in cmts.all():
2041 for cmt in cmts.all():
2042 grouped[cmt.revision].append(cmt)
2042 grouped[cmt.revision].append(cmt)
2043 return grouped
2043 return grouped
2044
2044
2045 def statuses(self, revisions=None):
2045 def statuses(self, revisions=None):
2046 """
2046 """
2047 Returns statuses for this repository
2047 Returns statuses for this repository
2048
2048
2049 :param revisions: list of revisions to get statuses for
2049 :param revisions: list of revisions to get statuses for
2050 """
2050 """
2051 statuses = ChangesetStatus.query()\
2051 statuses = ChangesetStatus.query()\
2052 .filter(ChangesetStatus.repo == self)\
2052 .filter(ChangesetStatus.repo == self)\
2053 .filter(ChangesetStatus.version == 0)
2053 .filter(ChangesetStatus.version == 0)
2054
2054
2055 if revisions:
2055 if revisions:
2056 # Try doing the filtering in chunks to avoid hitting limits
2056 # Try doing the filtering in chunks to avoid hitting limits
2057 size = 500
2057 size = 500
2058 status_results = []
2058 status_results = []
2059 for chunk in xrange(0, len(revisions), size):
2059 for chunk in xrange(0, len(revisions), size):
2060 status_results += statuses.filter(
2060 status_results += statuses.filter(
2061 ChangesetStatus.revision.in_(
2061 ChangesetStatus.revision.in_(
2062 revisions[chunk: chunk+size])
2062 revisions[chunk: chunk+size])
2063 ).all()
2063 ).all()
2064 else:
2064 else:
2065 status_results = statuses.all()
2065 status_results = statuses.all()
2066
2066
2067 grouped = {}
2067 grouped = {}
2068
2068
2069 # maybe we have open new pullrequest without a status?
2069 # maybe we have open new pullrequest without a status?
2070 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2070 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2071 status_lbl = ChangesetStatus.get_status_lbl(stat)
2071 status_lbl = ChangesetStatus.get_status_lbl(stat)
2072 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2072 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2073 for rev in pr.revisions:
2073 for rev in pr.revisions:
2074 pr_id = pr.pull_request_id
2074 pr_id = pr.pull_request_id
2075 pr_repo = pr.target_repo.repo_name
2075 pr_repo = pr.target_repo.repo_name
2076 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2076 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2077
2077
2078 for stat in status_results:
2078 for stat in status_results:
2079 pr_id = pr_repo = None
2079 pr_id = pr_repo = None
2080 if stat.pull_request:
2080 if stat.pull_request:
2081 pr_id = stat.pull_request.pull_request_id
2081 pr_id = stat.pull_request.pull_request_id
2082 pr_repo = stat.pull_request.target_repo.repo_name
2082 pr_repo = stat.pull_request.target_repo.repo_name
2083 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2083 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2084 pr_id, pr_repo]
2084 pr_id, pr_repo]
2085 return grouped
2085 return grouped
2086
2086
2087 # ==========================================================================
2087 # ==========================================================================
2088 # SCM CACHE INSTANCE
2088 # SCM CACHE INSTANCE
2089 # ==========================================================================
2089 # ==========================================================================
2090
2090
2091 def scm_instance(self, **kwargs):
2091 def scm_instance(self, **kwargs):
2092 import rhodecode
2092 import rhodecode
2093
2093
2094 # Passing a config will not hit the cache currently only used
2094 # Passing a config will not hit the cache currently only used
2095 # for repo2dbmapper
2095 # for repo2dbmapper
2096 config = kwargs.pop('config', None)
2096 config = kwargs.pop('config', None)
2097 cache = kwargs.pop('cache', None)
2097 cache = kwargs.pop('cache', None)
2098 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2098 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2099 # if cache is NOT defined use default global, else we have a full
2099 # if cache is NOT defined use default global, else we have a full
2100 # control over cache behaviour
2100 # control over cache behaviour
2101 if cache is None and full_cache and not config:
2101 if cache is None and full_cache and not config:
2102 return self._get_instance_cached()
2102 return self._get_instance_cached()
2103 return self._get_instance(cache=bool(cache), config=config)
2103 return self._get_instance(cache=bool(cache), config=config)
2104
2104
2105 def _get_instance_cached(self):
2105 def _get_instance_cached(self):
2106 @cache_region('long_term')
2106 @cache_region('long_term')
2107 def _get_repo(cache_key):
2107 def _get_repo(cache_key):
2108 return self._get_instance()
2108 return self._get_instance()
2109
2109
2110 invalidator_context = CacheKey.repo_context_cache(
2110 invalidator_context = CacheKey.repo_context_cache(
2111 _get_repo, self.repo_name, None, thread_scoped=True)
2111 _get_repo, self.repo_name, None, thread_scoped=True)
2112
2112
2113 with invalidator_context as context:
2113 with invalidator_context as context:
2114 context.invalidate()
2114 context.invalidate()
2115 repo = context.compute()
2115 repo = context.compute()
2116
2116
2117 return repo
2117 return repo
2118
2118
2119 def _get_instance(self, cache=True, config=None):
2119 def _get_instance(self, cache=True, config=None):
2120 config = config or self._config
2120 config = config or self._config
2121 custom_wire = {
2121 custom_wire = {
2122 'cache': cache # controls the vcs.remote cache
2122 'cache': cache # controls the vcs.remote cache
2123 }
2123 }
2124 repo = get_vcs_instance(
2124 repo = get_vcs_instance(
2125 repo_path=safe_str(self.repo_full_path),
2125 repo_path=safe_str(self.repo_full_path),
2126 config=config,
2126 config=config,
2127 with_wire=custom_wire,
2127 with_wire=custom_wire,
2128 create=False,
2128 create=False,
2129 _vcs_alias=self.repo_type)
2129 _vcs_alias=self.repo_type)
2130
2130
2131 return repo
2131 return repo
2132
2132
2133 def __json__(self):
2133 def __json__(self):
2134 return {'landing_rev': self.landing_rev}
2134 return {'landing_rev': self.landing_rev}
2135
2135
2136 def get_dict(self):
2136 def get_dict(self):
2137
2137
2138 # Since we transformed `repo_name` to a hybrid property, we need to
2138 # Since we transformed `repo_name` to a hybrid property, we need to
2139 # keep compatibility with the code which uses `repo_name` field.
2139 # keep compatibility with the code which uses `repo_name` field.
2140
2140
2141 result = super(Repository, self).get_dict()
2141 result = super(Repository, self).get_dict()
2142 result['repo_name'] = result.pop('_repo_name', None)
2142 result['repo_name'] = result.pop('_repo_name', None)
2143 return result
2143 return result
2144
2144
2145
2145
2146 class RepoGroup(Base, BaseModel):
2146 class RepoGroup(Base, BaseModel):
2147 __tablename__ = 'groups'
2147 __tablename__ = 'groups'
2148 __table_args__ = (
2148 __table_args__ = (
2149 UniqueConstraint('group_name', 'group_parent_id'),
2149 UniqueConstraint('group_name', 'group_parent_id'),
2150 CheckConstraint('group_id != group_parent_id'),
2150 CheckConstraint('group_id != group_parent_id'),
2151 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2151 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2152 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2152 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2153 )
2153 )
2154 __mapper_args__ = {'order_by': 'group_name'}
2154 __mapper_args__ = {'order_by': 'group_name'}
2155
2155
2156 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2156 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2157
2157
2158 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2158 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2159 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2159 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2160 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2160 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2161 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2161 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2162 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2162 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2163 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2163 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2164 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2164 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2165 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2165 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2166
2166
2167 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2167 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2168 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2168 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2169 parent_group = relationship('RepoGroup', remote_side=group_id)
2169 parent_group = relationship('RepoGroup', remote_side=group_id)
2170 user = relationship('User')
2170 user = relationship('User')
2171 integrations = relationship('Integration',
2171 integrations = relationship('Integration',
2172 cascade="all, delete, delete-orphan")
2172 cascade="all, delete, delete-orphan")
2173
2173
2174 def __init__(self, group_name='', parent_group=None):
2174 def __init__(self, group_name='', parent_group=None):
2175 self.group_name = group_name
2175 self.group_name = group_name
2176 self.parent_group = parent_group
2176 self.parent_group = parent_group
2177
2177
2178 def __unicode__(self):
2178 def __unicode__(self):
2179 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2179 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2180 self.group_name)
2180 self.group_name)
2181
2181
2182 @classmethod
2182 @classmethod
2183 def _generate_choice(cls, repo_group):
2183 def _generate_choice(cls, repo_group):
2184 from webhelpers.html import literal as _literal
2184 from webhelpers.html import literal as _literal
2185 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2185 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2186 return repo_group.group_id, _name(repo_group.full_path_splitted)
2186 return repo_group.group_id, _name(repo_group.full_path_splitted)
2187
2187
2188 @classmethod
2188 @classmethod
2189 def groups_choices(cls, groups=None, show_empty_group=True):
2189 def groups_choices(cls, groups=None, show_empty_group=True):
2190 if not groups:
2190 if not groups:
2191 groups = cls.query().all()
2191 groups = cls.query().all()
2192
2192
2193 repo_groups = []
2193 repo_groups = []
2194 if show_empty_group:
2194 if show_empty_group:
2195 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2195 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2196
2196
2197 repo_groups.extend([cls._generate_choice(x) for x in groups])
2197 repo_groups.extend([cls._generate_choice(x) for x in groups])
2198
2198
2199 repo_groups = sorted(
2199 repo_groups = sorted(
2200 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2200 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2201 return repo_groups
2201 return repo_groups
2202
2202
2203 @classmethod
2203 @classmethod
2204 def url_sep(cls):
2204 def url_sep(cls):
2205 return URL_SEP
2205 return URL_SEP
2206
2206
2207 @classmethod
2207 @classmethod
2208 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2208 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2209 if case_insensitive:
2209 if case_insensitive:
2210 gr = cls.query().filter(func.lower(cls.group_name)
2210 gr = cls.query().filter(func.lower(cls.group_name)
2211 == func.lower(group_name))
2211 == func.lower(group_name))
2212 else:
2212 else:
2213 gr = cls.query().filter(cls.group_name == group_name)
2213 gr = cls.query().filter(cls.group_name == group_name)
2214 if cache:
2214 if cache:
2215 name_key = _hash_key(group_name)
2215 name_key = _hash_key(group_name)
2216 gr = gr.options(
2216 gr = gr.options(
2217 FromCache("sql_cache_short", "get_group_%s" % name_key))
2217 FromCache("sql_cache_short", "get_group_%s" % name_key))
2218 return gr.scalar()
2218 return gr.scalar()
2219
2219
2220 @classmethod
2220 @classmethod
2221 def get_user_personal_repo_group(cls, user_id):
2221 def get_user_personal_repo_group(cls, user_id):
2222 user = User.get(user_id)
2222 user = User.get(user_id)
2223 if user.username == User.DEFAULT_USER:
2223 if user.username == User.DEFAULT_USER:
2224 return None
2224 return None
2225
2225
2226 return cls.query()\
2226 return cls.query()\
2227 .filter(cls.personal == true()) \
2227 .filter(cls.personal == true()) \
2228 .filter(cls.user == user).scalar()
2228 .filter(cls.user == user).scalar()
2229
2229
2230 @classmethod
2230 @classmethod
2231 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2231 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2232 case_insensitive=True):
2232 case_insensitive=True):
2233 q = RepoGroup.query()
2233 q = RepoGroup.query()
2234
2234
2235 if not isinstance(user_id, Optional):
2235 if not isinstance(user_id, Optional):
2236 q = q.filter(RepoGroup.user_id == user_id)
2236 q = q.filter(RepoGroup.user_id == user_id)
2237
2237
2238 if not isinstance(group_id, Optional):
2238 if not isinstance(group_id, Optional):
2239 q = q.filter(RepoGroup.group_parent_id == group_id)
2239 q = q.filter(RepoGroup.group_parent_id == group_id)
2240
2240
2241 if case_insensitive:
2241 if case_insensitive:
2242 q = q.order_by(func.lower(RepoGroup.group_name))
2242 q = q.order_by(func.lower(RepoGroup.group_name))
2243 else:
2243 else:
2244 q = q.order_by(RepoGroup.group_name)
2244 q = q.order_by(RepoGroup.group_name)
2245 return q.all()
2245 return q.all()
2246
2246
2247 @property
2247 @property
2248 def parents(self):
2248 def parents(self):
2249 parents_recursion_limit = 10
2249 parents_recursion_limit = 10
2250 groups = []
2250 groups = []
2251 if self.parent_group is None:
2251 if self.parent_group is None:
2252 return groups
2252 return groups
2253 cur_gr = self.parent_group
2253 cur_gr = self.parent_group
2254 groups.insert(0, cur_gr)
2254 groups.insert(0, cur_gr)
2255 cnt = 0
2255 cnt = 0
2256 while 1:
2256 while 1:
2257 cnt += 1
2257 cnt += 1
2258 gr = getattr(cur_gr, 'parent_group', None)
2258 gr = getattr(cur_gr, 'parent_group', None)
2259 cur_gr = cur_gr.parent_group
2259 cur_gr = cur_gr.parent_group
2260 if gr is None:
2260 if gr is None:
2261 break
2261 break
2262 if cnt == parents_recursion_limit:
2262 if cnt == parents_recursion_limit:
2263 # this will prevent accidental infinit loops
2263 # this will prevent accidental infinit loops
2264 log.error(('more than %s parents found for group %s, stopping '
2264 log.error(('more than %s parents found for group %s, stopping '
2265 'recursive parent fetching' % (parents_recursion_limit, self)))
2265 'recursive parent fetching' % (parents_recursion_limit, self)))
2266 break
2266 break
2267
2267
2268 groups.insert(0, gr)
2268 groups.insert(0, gr)
2269 return groups
2269 return groups
2270
2270
2271 @property
2271 @property
2272 def children(self):
2272 def children(self):
2273 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2273 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2274
2274
2275 @property
2275 @property
2276 def name(self):
2276 def name(self):
2277 return self.group_name.split(RepoGroup.url_sep())[-1]
2277 return self.group_name.split(RepoGroup.url_sep())[-1]
2278
2278
2279 @property
2279 @property
2280 def full_path(self):
2280 def full_path(self):
2281 return self.group_name
2281 return self.group_name
2282
2282
2283 @property
2283 @property
2284 def full_path_splitted(self):
2284 def full_path_splitted(self):
2285 return self.group_name.split(RepoGroup.url_sep())
2285 return self.group_name.split(RepoGroup.url_sep())
2286
2286
2287 @property
2287 @property
2288 def repositories(self):
2288 def repositories(self):
2289 return Repository.query()\
2289 return Repository.query()\
2290 .filter(Repository.group == self)\
2290 .filter(Repository.group == self)\
2291 .order_by(Repository.repo_name)
2291 .order_by(Repository.repo_name)
2292
2292
2293 @property
2293 @property
2294 def repositories_recursive_count(self):
2294 def repositories_recursive_count(self):
2295 cnt = self.repositories.count()
2295 cnt = self.repositories.count()
2296
2296
2297 def children_count(group):
2297 def children_count(group):
2298 cnt = 0
2298 cnt = 0
2299 for child in group.children:
2299 for child in group.children:
2300 cnt += child.repositories.count()
2300 cnt += child.repositories.count()
2301 cnt += children_count(child)
2301 cnt += children_count(child)
2302 return cnt
2302 return cnt
2303
2303
2304 return cnt + children_count(self)
2304 return cnt + children_count(self)
2305
2305
2306 def _recursive_objects(self, include_repos=True):
2306 def _recursive_objects(self, include_repos=True):
2307 all_ = []
2307 all_ = []
2308
2308
2309 def _get_members(root_gr):
2309 def _get_members(root_gr):
2310 if include_repos:
2310 if include_repos:
2311 for r in root_gr.repositories:
2311 for r in root_gr.repositories:
2312 all_.append(r)
2312 all_.append(r)
2313 childs = root_gr.children.all()
2313 childs = root_gr.children.all()
2314 if childs:
2314 if childs:
2315 for gr in childs:
2315 for gr in childs:
2316 all_.append(gr)
2316 all_.append(gr)
2317 _get_members(gr)
2317 _get_members(gr)
2318
2318
2319 _get_members(self)
2319 _get_members(self)
2320 return [self] + all_
2320 return [self] + all_
2321
2321
2322 def recursive_groups_and_repos(self):
2322 def recursive_groups_and_repos(self):
2323 """
2323 """
2324 Recursive return all groups, with repositories in those groups
2324 Recursive return all groups, with repositories in those groups
2325 """
2325 """
2326 return self._recursive_objects()
2326 return self._recursive_objects()
2327
2327
2328 def recursive_groups(self):
2328 def recursive_groups(self):
2329 """
2329 """
2330 Returns all children groups for this group including children of children
2330 Returns all children groups for this group including children of children
2331 """
2331 """
2332 return self._recursive_objects(include_repos=False)
2332 return self._recursive_objects(include_repos=False)
2333
2333
2334 def get_new_name(self, group_name):
2334 def get_new_name(self, group_name):
2335 """
2335 """
2336 returns new full group name based on parent and new name
2336 returns new full group name based on parent and new name
2337
2337
2338 :param group_name:
2338 :param group_name:
2339 """
2339 """
2340 path_prefix = (self.parent_group.full_path_splitted if
2340 path_prefix = (self.parent_group.full_path_splitted if
2341 self.parent_group else [])
2341 self.parent_group else [])
2342 return RepoGroup.url_sep().join(path_prefix + [group_name])
2342 return RepoGroup.url_sep().join(path_prefix + [group_name])
2343
2343
2344 def permissions(self, with_admins=True, with_owner=True):
2344 def permissions(self, with_admins=True, with_owner=True):
2345 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2345 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2346 q = q.options(joinedload(UserRepoGroupToPerm.group),
2346 q = q.options(joinedload(UserRepoGroupToPerm.group),
2347 joinedload(UserRepoGroupToPerm.user),
2347 joinedload(UserRepoGroupToPerm.user),
2348 joinedload(UserRepoGroupToPerm.permission),)
2348 joinedload(UserRepoGroupToPerm.permission),)
2349
2349
2350 # get owners and admins and permissions. We do a trick of re-writing
2350 # get owners and admins and permissions. We do a trick of re-writing
2351 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2351 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2352 # has a global reference and changing one object propagates to all
2352 # has a global reference and changing one object propagates to all
2353 # others. This means if admin is also an owner admin_row that change
2353 # others. This means if admin is also an owner admin_row that change
2354 # would propagate to both objects
2354 # would propagate to both objects
2355 perm_rows = []
2355 perm_rows = []
2356 for _usr in q.all():
2356 for _usr in q.all():
2357 usr = AttributeDict(_usr.user.get_dict())
2357 usr = AttributeDict(_usr.user.get_dict())
2358 usr.permission = _usr.permission.permission_name
2358 usr.permission = _usr.permission.permission_name
2359 perm_rows.append(usr)
2359 perm_rows.append(usr)
2360
2360
2361 # filter the perm rows by 'default' first and then sort them by
2361 # filter the perm rows by 'default' first and then sort them by
2362 # admin,write,read,none permissions sorted again alphabetically in
2362 # admin,write,read,none permissions sorted again alphabetically in
2363 # each group
2363 # each group
2364 perm_rows = sorted(perm_rows, key=display_sort)
2364 perm_rows = sorted(perm_rows, key=display_sort)
2365
2365
2366 _admin_perm = 'group.admin'
2366 _admin_perm = 'group.admin'
2367 owner_row = []
2367 owner_row = []
2368 if with_owner:
2368 if with_owner:
2369 usr = AttributeDict(self.user.get_dict())
2369 usr = AttributeDict(self.user.get_dict())
2370 usr.owner_row = True
2370 usr.owner_row = True
2371 usr.permission = _admin_perm
2371 usr.permission = _admin_perm
2372 owner_row.append(usr)
2372 owner_row.append(usr)
2373
2373
2374 super_admin_rows = []
2374 super_admin_rows = []
2375 if with_admins:
2375 if with_admins:
2376 for usr in User.get_all_super_admins():
2376 for usr in User.get_all_super_admins():
2377 # if this admin is also owner, don't double the record
2377 # if this admin is also owner, don't double the record
2378 if usr.user_id == owner_row[0].user_id:
2378 if usr.user_id == owner_row[0].user_id:
2379 owner_row[0].admin_row = True
2379 owner_row[0].admin_row = True
2380 else:
2380 else:
2381 usr = AttributeDict(usr.get_dict())
2381 usr = AttributeDict(usr.get_dict())
2382 usr.admin_row = True
2382 usr.admin_row = True
2383 usr.permission = _admin_perm
2383 usr.permission = _admin_perm
2384 super_admin_rows.append(usr)
2384 super_admin_rows.append(usr)
2385
2385
2386 return super_admin_rows + owner_row + perm_rows
2386 return super_admin_rows + owner_row + perm_rows
2387
2387
2388 def permission_user_groups(self):
2388 def permission_user_groups(self):
2389 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2389 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2390 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2390 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2391 joinedload(UserGroupRepoGroupToPerm.users_group),
2391 joinedload(UserGroupRepoGroupToPerm.users_group),
2392 joinedload(UserGroupRepoGroupToPerm.permission),)
2392 joinedload(UserGroupRepoGroupToPerm.permission),)
2393
2393
2394 perm_rows = []
2394 perm_rows = []
2395 for _user_group in q.all():
2395 for _user_group in q.all():
2396 usr = AttributeDict(_user_group.users_group.get_dict())
2396 usr = AttributeDict(_user_group.users_group.get_dict())
2397 usr.permission = _user_group.permission.permission_name
2397 usr.permission = _user_group.permission.permission_name
2398 perm_rows.append(usr)
2398 perm_rows.append(usr)
2399
2399
2400 return perm_rows
2400 return perm_rows
2401
2401
2402 def get_api_data(self):
2402 def get_api_data(self):
2403 """
2403 """
2404 Common function for generating api data
2404 Common function for generating api data
2405
2405
2406 """
2406 """
2407 group = self
2407 group = self
2408 data = {
2408 data = {
2409 'group_id': group.group_id,
2409 'group_id': group.group_id,
2410 'group_name': group.group_name,
2410 'group_name': group.group_name,
2411 'group_description': group.group_description,
2411 'group_description': group.group_description,
2412 'parent_group': group.parent_group.group_name if group.parent_group else None,
2412 'parent_group': group.parent_group.group_name if group.parent_group else None,
2413 'repositories': [x.repo_name for x in group.repositories],
2413 'repositories': [x.repo_name for x in group.repositories],
2414 'owner': group.user.username,
2414 'owner': group.user.username,
2415 }
2415 }
2416 return data
2416 return data
2417
2417
2418
2418
2419 class Permission(Base, BaseModel):
2419 class Permission(Base, BaseModel):
2420 __tablename__ = 'permissions'
2420 __tablename__ = 'permissions'
2421 __table_args__ = (
2421 __table_args__ = (
2422 Index('p_perm_name_idx', 'permission_name'),
2422 Index('p_perm_name_idx', 'permission_name'),
2423 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2423 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2424 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2424 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2425 )
2425 )
2426 PERMS = [
2426 PERMS = [
2427 ('hg.admin', _('RhodeCode Super Administrator')),
2427 ('hg.admin', _('RhodeCode Super Administrator')),
2428
2428
2429 ('repository.none', _('Repository no access')),
2429 ('repository.none', _('Repository no access')),
2430 ('repository.read', _('Repository read access')),
2430 ('repository.read', _('Repository read access')),
2431 ('repository.write', _('Repository write access')),
2431 ('repository.write', _('Repository write access')),
2432 ('repository.admin', _('Repository admin access')),
2432 ('repository.admin', _('Repository admin access')),
2433
2433
2434 ('group.none', _('Repository group no access')),
2434 ('group.none', _('Repository group no access')),
2435 ('group.read', _('Repository group read access')),
2435 ('group.read', _('Repository group read access')),
2436 ('group.write', _('Repository group write access')),
2436 ('group.write', _('Repository group write access')),
2437 ('group.admin', _('Repository group admin access')),
2437 ('group.admin', _('Repository group admin access')),
2438
2438
2439 ('usergroup.none', _('User group no access')),
2439 ('usergroup.none', _('User group no access')),
2440 ('usergroup.read', _('User group read access')),
2440 ('usergroup.read', _('User group read access')),
2441 ('usergroup.write', _('User group write access')),
2441 ('usergroup.write', _('User group write access')),
2442 ('usergroup.admin', _('User group admin access')),
2442 ('usergroup.admin', _('User group admin access')),
2443
2443
2444 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2444 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2445 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2445 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2446
2446
2447 ('hg.usergroup.create.false', _('User Group creation disabled')),
2447 ('hg.usergroup.create.false', _('User Group creation disabled')),
2448 ('hg.usergroup.create.true', _('User Group creation enabled')),
2448 ('hg.usergroup.create.true', _('User Group creation enabled')),
2449
2449
2450 ('hg.create.none', _('Repository creation disabled')),
2450 ('hg.create.none', _('Repository creation disabled')),
2451 ('hg.create.repository', _('Repository creation enabled')),
2451 ('hg.create.repository', _('Repository creation enabled')),
2452 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2452 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2453 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2453 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2454
2454
2455 ('hg.fork.none', _('Repository forking disabled')),
2455 ('hg.fork.none', _('Repository forking disabled')),
2456 ('hg.fork.repository', _('Repository forking enabled')),
2456 ('hg.fork.repository', _('Repository forking enabled')),
2457
2457
2458 ('hg.register.none', _('Registration disabled')),
2458 ('hg.register.none', _('Registration disabled')),
2459 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2459 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2460 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2460 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2461
2461
2462 ('hg.password_reset.enabled', _('Password reset enabled')),
2462 ('hg.password_reset.enabled', _('Password reset enabled')),
2463 ('hg.password_reset.hidden', _('Password reset hidden')),
2463 ('hg.password_reset.hidden', _('Password reset hidden')),
2464 ('hg.password_reset.disabled', _('Password reset disabled')),
2464 ('hg.password_reset.disabled', _('Password reset disabled')),
2465
2465
2466 ('hg.extern_activate.manual', _('Manual activation of external account')),
2466 ('hg.extern_activate.manual', _('Manual activation of external account')),
2467 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2467 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2468
2468
2469 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2469 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2470 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2470 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2471 ]
2471 ]
2472
2472
2473 # definition of system default permissions for DEFAULT user
2473 # definition of system default permissions for DEFAULT user
2474 DEFAULT_USER_PERMISSIONS = [
2474 DEFAULT_USER_PERMISSIONS = [
2475 'repository.read',
2475 'repository.read',
2476 'group.read',
2476 'group.read',
2477 'usergroup.read',
2477 'usergroup.read',
2478 'hg.create.repository',
2478 'hg.create.repository',
2479 'hg.repogroup.create.false',
2479 'hg.repogroup.create.false',
2480 'hg.usergroup.create.false',
2480 'hg.usergroup.create.false',
2481 'hg.create.write_on_repogroup.true',
2481 'hg.create.write_on_repogroup.true',
2482 'hg.fork.repository',
2482 'hg.fork.repository',
2483 'hg.register.manual_activate',
2483 'hg.register.manual_activate',
2484 'hg.password_reset.enabled',
2484 'hg.password_reset.enabled',
2485 'hg.extern_activate.auto',
2485 'hg.extern_activate.auto',
2486 'hg.inherit_default_perms.true',
2486 'hg.inherit_default_perms.true',
2487 ]
2487 ]
2488
2488
2489 # defines which permissions are more important higher the more important
2489 # defines which permissions are more important higher the more important
2490 # Weight defines which permissions are more important.
2490 # Weight defines which permissions are more important.
2491 # The higher number the more important.
2491 # The higher number the more important.
2492 PERM_WEIGHTS = {
2492 PERM_WEIGHTS = {
2493 'repository.none': 0,
2493 'repository.none': 0,
2494 'repository.read': 1,
2494 'repository.read': 1,
2495 'repository.write': 3,
2495 'repository.write': 3,
2496 'repository.admin': 4,
2496 'repository.admin': 4,
2497
2497
2498 'group.none': 0,
2498 'group.none': 0,
2499 'group.read': 1,
2499 'group.read': 1,
2500 'group.write': 3,
2500 'group.write': 3,
2501 'group.admin': 4,
2501 'group.admin': 4,
2502
2502
2503 'usergroup.none': 0,
2503 'usergroup.none': 0,
2504 'usergroup.read': 1,
2504 'usergroup.read': 1,
2505 'usergroup.write': 3,
2505 'usergroup.write': 3,
2506 'usergroup.admin': 4,
2506 'usergroup.admin': 4,
2507
2507
2508 'hg.repogroup.create.false': 0,
2508 'hg.repogroup.create.false': 0,
2509 'hg.repogroup.create.true': 1,
2509 'hg.repogroup.create.true': 1,
2510
2510
2511 'hg.usergroup.create.false': 0,
2511 'hg.usergroup.create.false': 0,
2512 'hg.usergroup.create.true': 1,
2512 'hg.usergroup.create.true': 1,
2513
2513
2514 'hg.fork.none': 0,
2514 'hg.fork.none': 0,
2515 'hg.fork.repository': 1,
2515 'hg.fork.repository': 1,
2516 'hg.create.none': 0,
2516 'hg.create.none': 0,
2517 'hg.create.repository': 1
2517 'hg.create.repository': 1
2518 }
2518 }
2519
2519
2520 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2520 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2521 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2521 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2522 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2522 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2523
2523
2524 def __unicode__(self):
2524 def __unicode__(self):
2525 return u"<%s('%s:%s')>" % (
2525 return u"<%s('%s:%s')>" % (
2526 self.__class__.__name__, self.permission_id, self.permission_name
2526 self.__class__.__name__, self.permission_id, self.permission_name
2527 )
2527 )
2528
2528
2529 @classmethod
2529 @classmethod
2530 def get_by_key(cls, key):
2530 def get_by_key(cls, key):
2531 return cls.query().filter(cls.permission_name == key).scalar()
2531 return cls.query().filter(cls.permission_name == key).scalar()
2532
2532
2533 @classmethod
2533 @classmethod
2534 def get_default_repo_perms(cls, user_id, repo_id=None):
2534 def get_default_repo_perms(cls, user_id, repo_id=None):
2535 q = Session().query(UserRepoToPerm, Repository, Permission)\
2535 q = Session().query(UserRepoToPerm, Repository, Permission)\
2536 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2536 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2537 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2537 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2538 .filter(UserRepoToPerm.user_id == user_id)
2538 .filter(UserRepoToPerm.user_id == user_id)
2539 if repo_id:
2539 if repo_id:
2540 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2540 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2541 return q.all()
2541 return q.all()
2542
2542
2543 @classmethod
2543 @classmethod
2544 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2544 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2545 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2545 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2546 .join(
2546 .join(
2547 Permission,
2547 Permission,
2548 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2548 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2549 .join(
2549 .join(
2550 Repository,
2550 Repository,
2551 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2551 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2552 .join(
2552 .join(
2553 UserGroup,
2553 UserGroup,
2554 UserGroupRepoToPerm.users_group_id ==
2554 UserGroupRepoToPerm.users_group_id ==
2555 UserGroup.users_group_id)\
2555 UserGroup.users_group_id)\
2556 .join(
2556 .join(
2557 UserGroupMember,
2557 UserGroupMember,
2558 UserGroupRepoToPerm.users_group_id ==
2558 UserGroupRepoToPerm.users_group_id ==
2559 UserGroupMember.users_group_id)\
2559 UserGroupMember.users_group_id)\
2560 .filter(
2560 .filter(
2561 UserGroupMember.user_id == user_id,
2561 UserGroupMember.user_id == user_id,
2562 UserGroup.users_group_active == true())
2562 UserGroup.users_group_active == true())
2563 if repo_id:
2563 if repo_id:
2564 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2564 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2565 return q.all()
2565 return q.all()
2566
2566
2567 @classmethod
2567 @classmethod
2568 def get_default_group_perms(cls, user_id, repo_group_id=None):
2568 def get_default_group_perms(cls, user_id, repo_group_id=None):
2569 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2569 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2570 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2570 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2571 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2571 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2572 .filter(UserRepoGroupToPerm.user_id == user_id)
2572 .filter(UserRepoGroupToPerm.user_id == user_id)
2573 if repo_group_id:
2573 if repo_group_id:
2574 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2574 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2575 return q.all()
2575 return q.all()
2576
2576
2577 @classmethod
2577 @classmethod
2578 def get_default_group_perms_from_user_group(
2578 def get_default_group_perms_from_user_group(
2579 cls, user_id, repo_group_id=None):
2579 cls, user_id, repo_group_id=None):
2580 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2580 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2581 .join(
2581 .join(
2582 Permission,
2582 Permission,
2583 UserGroupRepoGroupToPerm.permission_id ==
2583 UserGroupRepoGroupToPerm.permission_id ==
2584 Permission.permission_id)\
2584 Permission.permission_id)\
2585 .join(
2585 .join(
2586 RepoGroup,
2586 RepoGroup,
2587 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2587 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2588 .join(
2588 .join(
2589 UserGroup,
2589 UserGroup,
2590 UserGroupRepoGroupToPerm.users_group_id ==
2590 UserGroupRepoGroupToPerm.users_group_id ==
2591 UserGroup.users_group_id)\
2591 UserGroup.users_group_id)\
2592 .join(
2592 .join(
2593 UserGroupMember,
2593 UserGroupMember,
2594 UserGroupRepoGroupToPerm.users_group_id ==
2594 UserGroupRepoGroupToPerm.users_group_id ==
2595 UserGroupMember.users_group_id)\
2595 UserGroupMember.users_group_id)\
2596 .filter(
2596 .filter(
2597 UserGroupMember.user_id == user_id,
2597 UserGroupMember.user_id == user_id,
2598 UserGroup.users_group_active == true())
2598 UserGroup.users_group_active == true())
2599 if repo_group_id:
2599 if repo_group_id:
2600 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2600 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2601 return q.all()
2601 return q.all()
2602
2602
2603 @classmethod
2603 @classmethod
2604 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2604 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2605 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2605 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2606 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2606 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2607 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2607 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2608 .filter(UserUserGroupToPerm.user_id == user_id)
2608 .filter(UserUserGroupToPerm.user_id == user_id)
2609 if user_group_id:
2609 if user_group_id:
2610 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2610 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2611 return q.all()
2611 return q.all()
2612
2612
2613 @classmethod
2613 @classmethod
2614 def get_default_user_group_perms_from_user_group(
2614 def get_default_user_group_perms_from_user_group(
2615 cls, user_id, user_group_id=None):
2615 cls, user_id, user_group_id=None):
2616 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2616 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2617 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2617 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2618 .join(
2618 .join(
2619 Permission,
2619 Permission,
2620 UserGroupUserGroupToPerm.permission_id ==
2620 UserGroupUserGroupToPerm.permission_id ==
2621 Permission.permission_id)\
2621 Permission.permission_id)\
2622 .join(
2622 .join(
2623 TargetUserGroup,
2623 TargetUserGroup,
2624 UserGroupUserGroupToPerm.target_user_group_id ==
2624 UserGroupUserGroupToPerm.target_user_group_id ==
2625 TargetUserGroup.users_group_id)\
2625 TargetUserGroup.users_group_id)\
2626 .join(
2626 .join(
2627 UserGroup,
2627 UserGroup,
2628 UserGroupUserGroupToPerm.user_group_id ==
2628 UserGroupUserGroupToPerm.user_group_id ==
2629 UserGroup.users_group_id)\
2629 UserGroup.users_group_id)\
2630 .join(
2630 .join(
2631 UserGroupMember,
2631 UserGroupMember,
2632 UserGroupUserGroupToPerm.user_group_id ==
2632 UserGroupUserGroupToPerm.user_group_id ==
2633 UserGroupMember.users_group_id)\
2633 UserGroupMember.users_group_id)\
2634 .filter(
2634 .filter(
2635 UserGroupMember.user_id == user_id,
2635 UserGroupMember.user_id == user_id,
2636 UserGroup.users_group_active == true())
2636 UserGroup.users_group_active == true())
2637 if user_group_id:
2637 if user_group_id:
2638 q = q.filter(
2638 q = q.filter(
2639 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2639 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2640
2640
2641 return q.all()
2641 return q.all()
2642
2642
2643
2643
2644 class UserRepoToPerm(Base, BaseModel):
2644 class UserRepoToPerm(Base, BaseModel):
2645 __tablename__ = 'repo_to_perm'
2645 __tablename__ = 'repo_to_perm'
2646 __table_args__ = (
2646 __table_args__ = (
2647 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2647 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2648 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2648 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2649 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2649 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2650 )
2650 )
2651 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2651 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2652 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2652 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2653 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2653 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2654 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2654 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2655
2655
2656 user = relationship('User')
2656 user = relationship('User')
2657 repository = relationship('Repository')
2657 repository = relationship('Repository')
2658 permission = relationship('Permission')
2658 permission = relationship('Permission')
2659
2659
2660 @classmethod
2660 @classmethod
2661 def create(cls, user, repository, permission):
2661 def create(cls, user, repository, permission):
2662 n = cls()
2662 n = cls()
2663 n.user = user
2663 n.user = user
2664 n.repository = repository
2664 n.repository = repository
2665 n.permission = permission
2665 n.permission = permission
2666 Session().add(n)
2666 Session().add(n)
2667 return n
2667 return n
2668
2668
2669 def __unicode__(self):
2669 def __unicode__(self):
2670 return u'<%s => %s >' % (self.user, self.repository)
2670 return u'<%s => %s >' % (self.user, self.repository)
2671
2671
2672
2672
2673 class UserUserGroupToPerm(Base, BaseModel):
2673 class UserUserGroupToPerm(Base, BaseModel):
2674 __tablename__ = 'user_user_group_to_perm'
2674 __tablename__ = 'user_user_group_to_perm'
2675 __table_args__ = (
2675 __table_args__ = (
2676 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2676 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2677 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2677 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2678 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2678 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2679 )
2679 )
2680 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2680 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2681 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2681 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2682 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2682 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2683 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2683 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2684
2684
2685 user = relationship('User')
2685 user = relationship('User')
2686 user_group = relationship('UserGroup')
2686 user_group = relationship('UserGroup')
2687 permission = relationship('Permission')
2687 permission = relationship('Permission')
2688
2688
2689 @classmethod
2689 @classmethod
2690 def create(cls, user, user_group, permission):
2690 def create(cls, user, user_group, permission):
2691 n = cls()
2691 n = cls()
2692 n.user = user
2692 n.user = user
2693 n.user_group = user_group
2693 n.user_group = user_group
2694 n.permission = permission
2694 n.permission = permission
2695 Session().add(n)
2695 Session().add(n)
2696 return n
2696 return n
2697
2697
2698 def __unicode__(self):
2698 def __unicode__(self):
2699 return u'<%s => %s >' % (self.user, self.user_group)
2699 return u'<%s => %s >' % (self.user, self.user_group)
2700
2700
2701
2701
2702 class UserToPerm(Base, BaseModel):
2702 class UserToPerm(Base, BaseModel):
2703 __tablename__ = 'user_to_perm'
2703 __tablename__ = 'user_to_perm'
2704 __table_args__ = (
2704 __table_args__ = (
2705 UniqueConstraint('user_id', 'permission_id'),
2705 UniqueConstraint('user_id', 'permission_id'),
2706 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2706 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2707 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2707 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2708 )
2708 )
2709 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2709 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2710 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2710 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2711 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2711 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2712
2712
2713 user = relationship('User')
2713 user = relationship('User')
2714 permission = relationship('Permission', lazy='joined')
2714 permission = relationship('Permission', lazy='joined')
2715
2715
2716 def __unicode__(self):
2716 def __unicode__(self):
2717 return u'<%s => %s >' % (self.user, self.permission)
2717 return u'<%s => %s >' % (self.user, self.permission)
2718
2718
2719
2719
2720 class UserGroupRepoToPerm(Base, BaseModel):
2720 class UserGroupRepoToPerm(Base, BaseModel):
2721 __tablename__ = 'users_group_repo_to_perm'
2721 __tablename__ = 'users_group_repo_to_perm'
2722 __table_args__ = (
2722 __table_args__ = (
2723 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2723 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 )
2726 )
2727 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2727 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2728 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2728 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2729 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2729 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2730 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2730 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2731
2731
2732 users_group = relationship('UserGroup')
2732 users_group = relationship('UserGroup')
2733 permission = relationship('Permission')
2733 permission = relationship('Permission')
2734 repository = relationship('Repository')
2734 repository = relationship('Repository')
2735
2735
2736 @classmethod
2736 @classmethod
2737 def create(cls, users_group, repository, permission):
2737 def create(cls, users_group, repository, permission):
2738 n = cls()
2738 n = cls()
2739 n.users_group = users_group
2739 n.users_group = users_group
2740 n.repository = repository
2740 n.repository = repository
2741 n.permission = permission
2741 n.permission = permission
2742 Session().add(n)
2742 Session().add(n)
2743 return n
2743 return n
2744
2744
2745 def __unicode__(self):
2745 def __unicode__(self):
2746 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2746 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2747
2747
2748
2748
2749 class UserGroupUserGroupToPerm(Base, BaseModel):
2749 class UserGroupUserGroupToPerm(Base, BaseModel):
2750 __tablename__ = 'user_group_user_group_to_perm'
2750 __tablename__ = 'user_group_user_group_to_perm'
2751 __table_args__ = (
2751 __table_args__ = (
2752 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2752 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2753 CheckConstraint('target_user_group_id != user_group_id'),
2753 CheckConstraint('target_user_group_id != user_group_id'),
2754 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2755 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2755 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2756 )
2756 )
2757 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2757 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2758 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2758 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2759 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2759 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2760 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2760 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2761
2761
2762 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2762 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2763 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2763 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2764 permission = relationship('Permission')
2764 permission = relationship('Permission')
2765
2765
2766 @classmethod
2766 @classmethod
2767 def create(cls, target_user_group, user_group, permission):
2767 def create(cls, target_user_group, user_group, permission):
2768 n = cls()
2768 n = cls()
2769 n.target_user_group = target_user_group
2769 n.target_user_group = target_user_group
2770 n.user_group = user_group
2770 n.user_group = user_group
2771 n.permission = permission
2771 n.permission = permission
2772 Session().add(n)
2772 Session().add(n)
2773 return n
2773 return n
2774
2774
2775 def __unicode__(self):
2775 def __unicode__(self):
2776 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2776 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2777
2777
2778
2778
2779 class UserGroupToPerm(Base, BaseModel):
2779 class UserGroupToPerm(Base, BaseModel):
2780 __tablename__ = 'users_group_to_perm'
2780 __tablename__ = 'users_group_to_perm'
2781 __table_args__ = (
2781 __table_args__ = (
2782 UniqueConstraint('users_group_id', 'permission_id',),
2782 UniqueConstraint('users_group_id', 'permission_id',),
2783 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2783 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2784 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2784 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2785 )
2785 )
2786 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2786 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2787 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2787 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2788 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2788 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2789
2789
2790 users_group = relationship('UserGroup')
2790 users_group = relationship('UserGroup')
2791 permission = relationship('Permission')
2791 permission = relationship('Permission')
2792
2792
2793
2793
2794 class UserRepoGroupToPerm(Base, BaseModel):
2794 class UserRepoGroupToPerm(Base, BaseModel):
2795 __tablename__ = 'user_repo_group_to_perm'
2795 __tablename__ = 'user_repo_group_to_perm'
2796 __table_args__ = (
2796 __table_args__ = (
2797 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2797 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2798 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2798 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2799 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2799 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2800 )
2800 )
2801
2801
2802 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2802 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2803 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2803 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2804 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2804 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2805 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2805 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2806
2806
2807 user = relationship('User')
2807 user = relationship('User')
2808 group = relationship('RepoGroup')
2808 group = relationship('RepoGroup')
2809 permission = relationship('Permission')
2809 permission = relationship('Permission')
2810
2810
2811 @classmethod
2811 @classmethod
2812 def create(cls, user, repository_group, permission):
2812 def create(cls, user, repository_group, permission):
2813 n = cls()
2813 n = cls()
2814 n.user = user
2814 n.user = user
2815 n.group = repository_group
2815 n.group = repository_group
2816 n.permission = permission
2816 n.permission = permission
2817 Session().add(n)
2817 Session().add(n)
2818 return n
2818 return n
2819
2819
2820
2820
2821 class UserGroupRepoGroupToPerm(Base, BaseModel):
2821 class UserGroupRepoGroupToPerm(Base, BaseModel):
2822 __tablename__ = 'users_group_repo_group_to_perm'
2822 __tablename__ = 'users_group_repo_group_to_perm'
2823 __table_args__ = (
2823 __table_args__ = (
2824 UniqueConstraint('users_group_id', 'group_id'),
2824 UniqueConstraint('users_group_id', 'group_id'),
2825 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2825 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2826 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2826 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2827 )
2827 )
2828
2828
2829 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2829 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2830 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2830 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2831 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2831 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2832 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2832 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2833
2833
2834 users_group = relationship('UserGroup')
2834 users_group = relationship('UserGroup')
2835 permission = relationship('Permission')
2835 permission = relationship('Permission')
2836 group = relationship('RepoGroup')
2836 group = relationship('RepoGroup')
2837
2837
2838 @classmethod
2838 @classmethod
2839 def create(cls, user_group, repository_group, permission):
2839 def create(cls, user_group, repository_group, permission):
2840 n = cls()
2840 n = cls()
2841 n.users_group = user_group
2841 n.users_group = user_group
2842 n.group = repository_group
2842 n.group = repository_group
2843 n.permission = permission
2843 n.permission = permission
2844 Session().add(n)
2844 Session().add(n)
2845 return n
2845 return n
2846
2846
2847 def __unicode__(self):
2847 def __unicode__(self):
2848 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2848 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2849
2849
2850
2850
2851 class Statistics(Base, BaseModel):
2851 class Statistics(Base, BaseModel):
2852 __tablename__ = 'statistics'
2852 __tablename__ = 'statistics'
2853 __table_args__ = (
2853 __table_args__ = (
2854 UniqueConstraint('repository_id'),
2854 UniqueConstraint('repository_id'),
2855 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2855 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2856 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2856 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2857 )
2857 )
2858 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2858 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2859 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2859 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2860 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2860 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2861 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2861 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2862 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2862 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2863 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2863 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2864
2864
2865 repository = relationship('Repository', single_parent=True)
2865 repository = relationship('Repository', single_parent=True)
2866
2866
2867
2867
2868 class UserFollowing(Base, BaseModel):
2868 class UserFollowing(Base, BaseModel):
2869 __tablename__ = 'user_followings'
2869 __tablename__ = 'user_followings'
2870 __table_args__ = (
2870 __table_args__ = (
2871 UniqueConstraint('user_id', 'follows_repository_id'),
2871 UniqueConstraint('user_id', 'follows_repository_id'),
2872 UniqueConstraint('user_id', 'follows_user_id'),
2872 UniqueConstraint('user_id', 'follows_user_id'),
2873 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2873 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2874 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2874 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2875 )
2875 )
2876
2876
2877 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2877 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2878 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2878 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2879 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2879 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2880 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2880 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2881 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2881 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2882
2882
2883 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2883 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2884
2884
2885 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2885 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2886 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2886 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2887
2887
2888 @classmethod
2888 @classmethod
2889 def get_repo_followers(cls, repo_id):
2889 def get_repo_followers(cls, repo_id):
2890 return cls.query().filter(cls.follows_repo_id == repo_id)
2890 return cls.query().filter(cls.follows_repo_id == repo_id)
2891
2891
2892
2892
2893 class CacheKey(Base, BaseModel):
2893 class CacheKey(Base, BaseModel):
2894 __tablename__ = 'cache_invalidation'
2894 __tablename__ = 'cache_invalidation'
2895 __table_args__ = (
2895 __table_args__ = (
2896 UniqueConstraint('cache_key'),
2896 UniqueConstraint('cache_key'),
2897 Index('key_idx', 'cache_key'),
2897 Index('key_idx', 'cache_key'),
2898 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2898 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2899 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2899 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2900 )
2900 )
2901 CACHE_TYPE_ATOM = 'ATOM'
2901 CACHE_TYPE_ATOM = 'ATOM'
2902 CACHE_TYPE_RSS = 'RSS'
2902 CACHE_TYPE_RSS = 'RSS'
2903 CACHE_TYPE_README = 'README'
2903 CACHE_TYPE_README = 'README'
2904
2904
2905 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2905 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2906 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2906 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2907 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2907 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2908 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2908 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2909
2909
2910 def __init__(self, cache_key, cache_args=''):
2910 def __init__(self, cache_key, cache_args=''):
2911 self.cache_key = cache_key
2911 self.cache_key = cache_key
2912 self.cache_args = cache_args
2912 self.cache_args = cache_args
2913 self.cache_active = False
2913 self.cache_active = False
2914
2914
2915 def __unicode__(self):
2915 def __unicode__(self):
2916 return u"<%s('%s:%s[%s]')>" % (
2916 return u"<%s('%s:%s[%s]')>" % (
2917 self.__class__.__name__,
2917 self.__class__.__name__,
2918 self.cache_id, self.cache_key, self.cache_active)
2918 self.cache_id, self.cache_key, self.cache_active)
2919
2919
2920 def _cache_key_partition(self):
2920 def _cache_key_partition(self):
2921 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2921 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2922 return prefix, repo_name, suffix
2922 return prefix, repo_name, suffix
2923
2923
2924 def get_prefix(self):
2924 def get_prefix(self):
2925 """
2925 """
2926 Try to extract prefix from existing cache key. The key could consist
2926 Try to extract prefix from existing cache key. The key could consist
2927 of prefix, repo_name, suffix
2927 of prefix, repo_name, suffix
2928 """
2928 """
2929 # this returns prefix, repo_name, suffix
2929 # this returns prefix, repo_name, suffix
2930 return self._cache_key_partition()[0]
2930 return self._cache_key_partition()[0]
2931
2931
2932 def get_suffix(self):
2932 def get_suffix(self):
2933 """
2933 """
2934 get suffix that might have been used in _get_cache_key to
2934 get suffix that might have been used in _get_cache_key to
2935 generate self.cache_key. Only used for informational purposes
2935 generate self.cache_key. Only used for informational purposes
2936 in repo_edit.mako.
2936 in repo_edit.mako.
2937 """
2937 """
2938 # prefix, repo_name, suffix
2938 # prefix, repo_name, suffix
2939 return self._cache_key_partition()[2]
2939 return self._cache_key_partition()[2]
2940
2940
2941 @classmethod
2941 @classmethod
2942 def delete_all_cache(cls):
2942 def delete_all_cache(cls):
2943 """
2943 """
2944 Delete all cache keys from database.
2944 Delete all cache keys from database.
2945 Should only be run when all instances are down and all entries
2945 Should only be run when all instances are down and all entries
2946 thus stale.
2946 thus stale.
2947 """
2947 """
2948 cls.query().delete()
2948 cls.query().delete()
2949 Session().commit()
2949 Session().commit()
2950
2950
2951 @classmethod
2951 @classmethod
2952 def get_cache_key(cls, repo_name, cache_type):
2952 def get_cache_key(cls, repo_name, cache_type):
2953 """
2953 """
2954
2954
2955 Generate a cache key for this process of RhodeCode instance.
2955 Generate a cache key for this process of RhodeCode instance.
2956 Prefix most likely will be process id or maybe explicitly set
2956 Prefix most likely will be process id or maybe explicitly set
2957 instance_id from .ini file.
2957 instance_id from .ini file.
2958 """
2958 """
2959 import rhodecode
2959 import rhodecode
2960 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2960 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2961
2961
2962 repo_as_unicode = safe_unicode(repo_name)
2962 repo_as_unicode = safe_unicode(repo_name)
2963 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2963 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2964 if cache_type else repo_as_unicode
2964 if cache_type else repo_as_unicode
2965
2965
2966 return u'{}{}'.format(prefix, key)
2966 return u'{}{}'.format(prefix, key)
2967
2967
2968 @classmethod
2968 @classmethod
2969 def set_invalidate(cls, repo_name, delete=False):
2969 def set_invalidate(cls, repo_name, delete=False):
2970 """
2970 """
2971 Mark all caches of a repo as invalid in the database.
2971 Mark all caches of a repo as invalid in the database.
2972 """
2972 """
2973
2973
2974 try:
2974 try:
2975 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2975 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2976 if delete:
2976 if delete:
2977 log.debug('cache objects deleted for repo %s',
2977 log.debug('cache objects deleted for repo %s',
2978 safe_str(repo_name))
2978 safe_str(repo_name))
2979 qry.delete()
2979 qry.delete()
2980 else:
2980 else:
2981 log.debug('cache objects marked as invalid for repo %s',
2981 log.debug('cache objects marked as invalid for repo %s',
2982 safe_str(repo_name))
2982 safe_str(repo_name))
2983 qry.update({"cache_active": False})
2983 qry.update({"cache_active": False})
2984
2984
2985 Session().commit()
2985 Session().commit()
2986 except Exception:
2986 except Exception:
2987 log.exception(
2987 log.exception(
2988 'Cache key invalidation failed for repository %s',
2988 'Cache key invalidation failed for repository %s',
2989 safe_str(repo_name))
2989 safe_str(repo_name))
2990 Session().rollback()
2990 Session().rollback()
2991
2991
2992 @classmethod
2992 @classmethod
2993 def get_active_cache(cls, cache_key):
2993 def get_active_cache(cls, cache_key):
2994 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2994 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2995 if inv_obj:
2995 if inv_obj:
2996 return inv_obj
2996 return inv_obj
2997 return None
2997 return None
2998
2998
2999 @classmethod
2999 @classmethod
3000 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3000 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3001 thread_scoped=False):
3001 thread_scoped=False):
3002 """
3002 """
3003 @cache_region('long_term')
3003 @cache_region('long_term')
3004 def _heavy_calculation(cache_key):
3004 def _heavy_calculation(cache_key):
3005 return 'result'
3005 return 'result'
3006
3006
3007 cache_context = CacheKey.repo_context_cache(
3007 cache_context = CacheKey.repo_context_cache(
3008 _heavy_calculation, repo_name, cache_type)
3008 _heavy_calculation, repo_name, cache_type)
3009
3009
3010 with cache_context as context:
3010 with cache_context as context:
3011 context.invalidate()
3011 context.invalidate()
3012 computed = context.compute()
3012 computed = context.compute()
3013
3013
3014 assert computed == 'result'
3014 assert computed == 'result'
3015 """
3015 """
3016 from rhodecode.lib import caches
3016 from rhodecode.lib import caches
3017 return caches.InvalidationContext(
3017 return caches.InvalidationContext(
3018 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3018 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3019
3019
3020
3020
3021 class ChangesetComment(Base, BaseModel):
3021 class ChangesetComment(Base, BaseModel):
3022 __tablename__ = 'changeset_comments'
3022 __tablename__ = 'changeset_comments'
3023 __table_args__ = (
3023 __table_args__ = (
3024 Index('cc_revision_idx', 'revision'),
3024 Index('cc_revision_idx', 'revision'),
3025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3027 )
3027 )
3028
3028
3029 COMMENT_OUTDATED = u'comment_outdated'
3029 COMMENT_OUTDATED = u'comment_outdated'
3030 COMMENT_TYPE_NOTE = u'note'
3030 COMMENT_TYPE_NOTE = u'note'
3031 COMMENT_TYPE_TODO = u'todo'
3031 COMMENT_TYPE_TODO = u'todo'
3032 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3032 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3033
3033
3034 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3034 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3035 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3035 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3036 revision = Column('revision', String(40), nullable=True)
3036 revision = Column('revision', String(40), nullable=True)
3037 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3037 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3038 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3038 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3039 line_no = Column('line_no', Unicode(10), nullable=True)
3039 line_no = Column('line_no', Unicode(10), nullable=True)
3040 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3040 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3041 f_path = Column('f_path', Unicode(1000), nullable=True)
3041 f_path = Column('f_path', Unicode(1000), nullable=True)
3042 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3042 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3043 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3043 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3044 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3044 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3045 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3045 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3046 renderer = Column('renderer', Unicode(64), nullable=True)
3046 renderer = Column('renderer', Unicode(64), nullable=True)
3047 display_state = Column('display_state', Unicode(128), nullable=True)
3047 display_state = Column('display_state', Unicode(128), nullable=True)
3048
3048
3049 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3049 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3050 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3050 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3051 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3051 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3052 author = relationship('User', lazy='joined')
3052 author = relationship('User', lazy='joined')
3053 repo = relationship('Repository')
3053 repo = relationship('Repository')
3054 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3054 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3055 pull_request = relationship('PullRequest', lazy='joined')
3055 pull_request = relationship('PullRequest', lazy='joined')
3056 pull_request_version = relationship('PullRequestVersion')
3056 pull_request_version = relationship('PullRequestVersion')
3057
3057
3058 @classmethod
3058 @classmethod
3059 def get_users(cls, revision=None, pull_request_id=None):
3059 def get_users(cls, revision=None, pull_request_id=None):
3060 """
3060 """
3061 Returns user associated with this ChangesetComment. ie those
3061 Returns user associated with this ChangesetComment. ie those
3062 who actually commented
3062 who actually commented
3063
3063
3064 :param cls:
3064 :param cls:
3065 :param revision:
3065 :param revision:
3066 """
3066 """
3067 q = Session().query(User)\
3067 q = Session().query(User)\
3068 .join(ChangesetComment.author)
3068 .join(ChangesetComment.author)
3069 if revision:
3069 if revision:
3070 q = q.filter(cls.revision == revision)
3070 q = q.filter(cls.revision == revision)
3071 elif pull_request_id:
3071 elif pull_request_id:
3072 q = q.filter(cls.pull_request_id == pull_request_id)
3072 q = q.filter(cls.pull_request_id == pull_request_id)
3073 return q.all()
3073 return q.all()
3074
3074
3075 @classmethod
3075 @classmethod
3076 def get_index_from_version(cls, pr_version, versions):
3076 def get_index_from_version(cls, pr_version, versions):
3077 num_versions = [x.pull_request_version_id for x in versions]
3077 num_versions = [x.pull_request_version_id for x in versions]
3078 try:
3078 try:
3079 return num_versions.index(pr_version) +1
3079 return num_versions.index(pr_version) +1
3080 except (IndexError, ValueError):
3080 except (IndexError, ValueError):
3081 return
3081 return
3082
3082
3083 @property
3083 @property
3084 def outdated(self):
3084 def outdated(self):
3085 return self.display_state == self.COMMENT_OUTDATED
3085 return self.display_state == self.COMMENT_OUTDATED
3086
3086
3087 def outdated_at_version(self, version):
3087 def outdated_at_version(self, version):
3088 """
3088 """
3089 Checks if comment is outdated for given pull request version
3089 Checks if comment is outdated for given pull request version
3090 """
3090 """
3091 return self.outdated and self.pull_request_version_id != version
3091 return self.outdated and self.pull_request_version_id != version
3092
3092
3093 def older_than_version(self, version):
3093 def older_than_version(self, version):
3094 """
3094 """
3095 Checks if comment is made from previous version than given
3095 Checks if comment is made from previous version than given
3096 """
3096 """
3097 if version is None:
3097 if version is None:
3098 return self.pull_request_version_id is not None
3098 return self.pull_request_version_id is not None
3099
3099
3100 return self.pull_request_version_id < version
3100 return self.pull_request_version_id < version
3101
3101
3102 @property
3102 @property
3103 def resolved(self):
3103 def resolved(self):
3104 return self.resolved_by[0] if self.resolved_by else None
3104 return self.resolved_by[0] if self.resolved_by else None
3105
3105
3106 @property
3106 @property
3107 def is_todo(self):
3107 def is_todo(self):
3108 return self.comment_type == self.COMMENT_TYPE_TODO
3108 return self.comment_type == self.COMMENT_TYPE_TODO
3109
3109
3110 @property
3111 def is_inline(self):
3112 return self.line_no and self.f_path
3113
3110 def get_index_version(self, versions):
3114 def get_index_version(self, versions):
3111 return self.get_index_from_version(
3115 return self.get_index_from_version(
3112 self.pull_request_version_id, versions)
3116 self.pull_request_version_id, versions)
3113
3117
3114 def __repr__(self):
3118 def __repr__(self):
3115 if self.comment_id:
3119 if self.comment_id:
3116 return '<DB:Comment #%s>' % self.comment_id
3120 return '<DB:Comment #%s>' % self.comment_id
3117 else:
3121 else:
3118 return '<DB:Comment at %#x>' % id(self)
3122 return '<DB:Comment at %#x>' % id(self)
3119
3123
3120
3124
3121 class ChangesetStatus(Base, BaseModel):
3125 class ChangesetStatus(Base, BaseModel):
3122 __tablename__ = 'changeset_statuses'
3126 __tablename__ = 'changeset_statuses'
3123 __table_args__ = (
3127 __table_args__ = (
3124 Index('cs_revision_idx', 'revision'),
3128 Index('cs_revision_idx', 'revision'),
3125 Index('cs_version_idx', 'version'),
3129 Index('cs_version_idx', 'version'),
3126 UniqueConstraint('repo_id', 'revision', 'version'),
3130 UniqueConstraint('repo_id', 'revision', 'version'),
3127 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3131 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3128 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3132 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3129 )
3133 )
3130 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3134 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3131 STATUS_APPROVED = 'approved'
3135 STATUS_APPROVED = 'approved'
3132 STATUS_REJECTED = 'rejected'
3136 STATUS_REJECTED = 'rejected'
3133 STATUS_UNDER_REVIEW = 'under_review'
3137 STATUS_UNDER_REVIEW = 'under_review'
3134
3138
3135 STATUSES = [
3139 STATUSES = [
3136 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3140 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3137 (STATUS_APPROVED, _("Approved")),
3141 (STATUS_APPROVED, _("Approved")),
3138 (STATUS_REJECTED, _("Rejected")),
3142 (STATUS_REJECTED, _("Rejected")),
3139 (STATUS_UNDER_REVIEW, _("Under Review")),
3143 (STATUS_UNDER_REVIEW, _("Under Review")),
3140 ]
3144 ]
3141
3145
3142 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3146 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3143 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3147 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3144 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3148 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3145 revision = Column('revision', String(40), nullable=False)
3149 revision = Column('revision', String(40), nullable=False)
3146 status = Column('status', String(128), nullable=False, default=DEFAULT)
3150 status = Column('status', String(128), nullable=False, default=DEFAULT)
3147 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3151 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3148 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3152 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3149 version = Column('version', Integer(), nullable=False, default=0)
3153 version = Column('version', Integer(), nullable=False, default=0)
3150 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3154 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3151
3155
3152 author = relationship('User', lazy='joined')
3156 author = relationship('User', lazy='joined')
3153 repo = relationship('Repository')
3157 repo = relationship('Repository')
3154 comment = relationship('ChangesetComment', lazy='joined')
3158 comment = relationship('ChangesetComment', lazy='joined')
3155 pull_request = relationship('PullRequest', lazy='joined')
3159 pull_request = relationship('PullRequest', lazy='joined')
3156
3160
3157 def __unicode__(self):
3161 def __unicode__(self):
3158 return u"<%s('%s[v%s]:%s')>" % (
3162 return u"<%s('%s[v%s]:%s')>" % (
3159 self.__class__.__name__,
3163 self.__class__.__name__,
3160 self.status, self.version, self.author
3164 self.status, self.version, self.author
3161 )
3165 )
3162
3166
3163 @classmethod
3167 @classmethod
3164 def get_status_lbl(cls, value):
3168 def get_status_lbl(cls, value):
3165 return dict(cls.STATUSES).get(value)
3169 return dict(cls.STATUSES).get(value)
3166
3170
3167 @property
3171 @property
3168 def status_lbl(self):
3172 def status_lbl(self):
3169 return ChangesetStatus.get_status_lbl(self.status)
3173 return ChangesetStatus.get_status_lbl(self.status)
3170
3174
3171
3175
3172 class _PullRequestBase(BaseModel):
3176 class _PullRequestBase(BaseModel):
3173 """
3177 """
3174 Common attributes of pull request and version entries.
3178 Common attributes of pull request and version entries.
3175 """
3179 """
3176
3180
3177 # .status values
3181 # .status values
3178 STATUS_NEW = u'new'
3182 STATUS_NEW = u'new'
3179 STATUS_OPEN = u'open'
3183 STATUS_OPEN = u'open'
3180 STATUS_CLOSED = u'closed'
3184 STATUS_CLOSED = u'closed'
3181
3185
3182 title = Column('title', Unicode(255), nullable=True)
3186 title = Column('title', Unicode(255), nullable=True)
3183 description = Column(
3187 description = Column(
3184 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3188 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3185 nullable=True)
3189 nullable=True)
3186 # new/open/closed status of pull request (not approve/reject/etc)
3190 # new/open/closed status of pull request (not approve/reject/etc)
3187 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3191 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3188 created_on = Column(
3192 created_on = Column(
3189 'created_on', DateTime(timezone=False), nullable=False,
3193 'created_on', DateTime(timezone=False), nullable=False,
3190 default=datetime.datetime.now)
3194 default=datetime.datetime.now)
3191 updated_on = Column(
3195 updated_on = Column(
3192 'updated_on', DateTime(timezone=False), nullable=False,
3196 'updated_on', DateTime(timezone=False), nullable=False,
3193 default=datetime.datetime.now)
3197 default=datetime.datetime.now)
3194
3198
3195 @declared_attr
3199 @declared_attr
3196 def user_id(cls):
3200 def user_id(cls):
3197 return Column(
3201 return Column(
3198 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3202 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3199 unique=None)
3203 unique=None)
3200
3204
3201 # 500 revisions max
3205 # 500 revisions max
3202 _revisions = Column(
3206 _revisions = Column(
3203 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3207 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3204
3208
3205 @declared_attr
3209 @declared_attr
3206 def source_repo_id(cls):
3210 def source_repo_id(cls):
3207 # TODO: dan: rename column to source_repo_id
3211 # TODO: dan: rename column to source_repo_id
3208 return Column(
3212 return Column(
3209 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3213 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3210 nullable=False)
3214 nullable=False)
3211
3215
3212 source_ref = Column('org_ref', Unicode(255), nullable=False)
3216 source_ref = Column('org_ref', Unicode(255), nullable=False)
3213
3217
3214 @declared_attr
3218 @declared_attr
3215 def target_repo_id(cls):
3219 def target_repo_id(cls):
3216 # TODO: dan: rename column to target_repo_id
3220 # TODO: dan: rename column to target_repo_id
3217 return Column(
3221 return Column(
3218 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3222 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3219 nullable=False)
3223 nullable=False)
3220
3224
3221 target_ref = Column('other_ref', Unicode(255), nullable=False)
3225 target_ref = Column('other_ref', Unicode(255), nullable=False)
3222 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3226 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3223
3227
3224 # TODO: dan: rename column to last_merge_source_rev
3228 # TODO: dan: rename column to last_merge_source_rev
3225 _last_merge_source_rev = Column(
3229 _last_merge_source_rev = Column(
3226 'last_merge_org_rev', String(40), nullable=True)
3230 'last_merge_org_rev', String(40), nullable=True)
3227 # TODO: dan: rename column to last_merge_target_rev
3231 # TODO: dan: rename column to last_merge_target_rev
3228 _last_merge_target_rev = Column(
3232 _last_merge_target_rev = Column(
3229 'last_merge_other_rev', String(40), nullable=True)
3233 'last_merge_other_rev', String(40), nullable=True)
3230 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3234 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3231 merge_rev = Column('merge_rev', String(40), nullable=True)
3235 merge_rev = Column('merge_rev', String(40), nullable=True)
3232
3236
3233 reviewer_data = Column(
3237 reviewer_data = Column(
3234 'reviewer_data_json', MutationObj.as_mutable(
3238 'reviewer_data_json', MutationObj.as_mutable(
3235 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3239 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3236
3240
3237 @property
3241 @property
3238 def reviewer_data_json(self):
3242 def reviewer_data_json(self):
3239 return json.dumps(self.reviewer_data)
3243 return json.dumps(self.reviewer_data)
3240
3244
3241 @hybrid_property
3245 @hybrid_property
3242 def revisions(self):
3246 def revisions(self):
3243 return self._revisions.split(':') if self._revisions else []
3247 return self._revisions.split(':') if self._revisions else []
3244
3248
3245 @revisions.setter
3249 @revisions.setter
3246 def revisions(self, val):
3250 def revisions(self, val):
3247 self._revisions = ':'.join(val)
3251 self._revisions = ':'.join(val)
3248
3252
3249 @declared_attr
3253 @declared_attr
3250 def author(cls):
3254 def author(cls):
3251 return relationship('User', lazy='joined')
3255 return relationship('User', lazy='joined')
3252
3256
3253 @declared_attr
3257 @declared_attr
3254 def source_repo(cls):
3258 def source_repo(cls):
3255 return relationship(
3259 return relationship(
3256 'Repository',
3260 'Repository',
3257 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3261 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3258
3262
3259 @property
3263 @property
3260 def source_ref_parts(self):
3264 def source_ref_parts(self):
3261 return self.unicode_to_reference(self.source_ref)
3265 return self.unicode_to_reference(self.source_ref)
3262
3266
3263 @declared_attr
3267 @declared_attr
3264 def target_repo(cls):
3268 def target_repo(cls):
3265 return relationship(
3269 return relationship(
3266 'Repository',
3270 'Repository',
3267 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3271 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3268
3272
3269 @property
3273 @property
3270 def target_ref_parts(self):
3274 def target_ref_parts(self):
3271 return self.unicode_to_reference(self.target_ref)
3275 return self.unicode_to_reference(self.target_ref)
3272
3276
3273 @property
3277 @property
3274 def shadow_merge_ref(self):
3278 def shadow_merge_ref(self):
3275 return self.unicode_to_reference(self._shadow_merge_ref)
3279 return self.unicode_to_reference(self._shadow_merge_ref)
3276
3280
3277 @shadow_merge_ref.setter
3281 @shadow_merge_ref.setter
3278 def shadow_merge_ref(self, ref):
3282 def shadow_merge_ref(self, ref):
3279 self._shadow_merge_ref = self.reference_to_unicode(ref)
3283 self._shadow_merge_ref = self.reference_to_unicode(ref)
3280
3284
3281 def unicode_to_reference(self, raw):
3285 def unicode_to_reference(self, raw):
3282 """
3286 """
3283 Convert a unicode (or string) to a reference object.
3287 Convert a unicode (or string) to a reference object.
3284 If unicode evaluates to False it returns None.
3288 If unicode evaluates to False it returns None.
3285 """
3289 """
3286 if raw:
3290 if raw:
3287 refs = raw.split(':')
3291 refs = raw.split(':')
3288 return Reference(*refs)
3292 return Reference(*refs)
3289 else:
3293 else:
3290 return None
3294 return None
3291
3295
3292 def reference_to_unicode(self, ref):
3296 def reference_to_unicode(self, ref):
3293 """
3297 """
3294 Convert a reference object to unicode.
3298 Convert a reference object to unicode.
3295 If reference is None it returns None.
3299 If reference is None it returns None.
3296 """
3300 """
3297 if ref:
3301 if ref:
3298 return u':'.join(ref)
3302 return u':'.join(ref)
3299 else:
3303 else:
3300 return None
3304 return None
3301
3305
3302 def get_api_data(self):
3306 def get_api_data(self):
3303 from pylons import url
3307 from pylons import url
3304 from rhodecode.model.pull_request import PullRequestModel
3308 from rhodecode.model.pull_request import PullRequestModel
3305 pull_request = self
3309 pull_request = self
3306 merge_status = PullRequestModel().merge_status(pull_request)
3310 merge_status = PullRequestModel().merge_status(pull_request)
3307
3311
3308 pull_request_url = url(
3312 pull_request_url = url(
3309 'pullrequest_show', repo_name=self.target_repo.repo_name,
3313 'pullrequest_show', repo_name=self.target_repo.repo_name,
3310 pull_request_id=self.pull_request_id, qualified=True)
3314 pull_request_id=self.pull_request_id, qualified=True)
3311
3315
3312 merge_data = {
3316 merge_data = {
3313 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3317 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3314 'reference': (
3318 'reference': (
3315 pull_request.shadow_merge_ref._asdict()
3319 pull_request.shadow_merge_ref._asdict()
3316 if pull_request.shadow_merge_ref else None),
3320 if pull_request.shadow_merge_ref else None),
3317 }
3321 }
3318
3322
3319 data = {
3323 data = {
3320 'pull_request_id': pull_request.pull_request_id,
3324 'pull_request_id': pull_request.pull_request_id,
3321 'url': pull_request_url,
3325 'url': pull_request_url,
3322 'title': pull_request.title,
3326 'title': pull_request.title,
3323 'description': pull_request.description,
3327 'description': pull_request.description,
3324 'status': pull_request.status,
3328 'status': pull_request.status,
3325 'created_on': pull_request.created_on,
3329 'created_on': pull_request.created_on,
3326 'updated_on': pull_request.updated_on,
3330 'updated_on': pull_request.updated_on,
3327 'commit_ids': pull_request.revisions,
3331 'commit_ids': pull_request.revisions,
3328 'review_status': pull_request.calculated_review_status(),
3332 'review_status': pull_request.calculated_review_status(),
3329 'mergeable': {
3333 'mergeable': {
3330 'status': merge_status[0],
3334 'status': merge_status[0],
3331 'message': unicode(merge_status[1]),
3335 'message': unicode(merge_status[1]),
3332 },
3336 },
3333 'source': {
3337 'source': {
3334 'clone_url': pull_request.source_repo.clone_url(),
3338 'clone_url': pull_request.source_repo.clone_url(),
3335 'repository': pull_request.source_repo.repo_name,
3339 'repository': pull_request.source_repo.repo_name,
3336 'reference': {
3340 'reference': {
3337 'name': pull_request.source_ref_parts.name,
3341 'name': pull_request.source_ref_parts.name,
3338 'type': pull_request.source_ref_parts.type,
3342 'type': pull_request.source_ref_parts.type,
3339 'commit_id': pull_request.source_ref_parts.commit_id,
3343 'commit_id': pull_request.source_ref_parts.commit_id,
3340 },
3344 },
3341 },
3345 },
3342 'target': {
3346 'target': {
3343 'clone_url': pull_request.target_repo.clone_url(),
3347 'clone_url': pull_request.target_repo.clone_url(),
3344 'repository': pull_request.target_repo.repo_name,
3348 'repository': pull_request.target_repo.repo_name,
3345 'reference': {
3349 'reference': {
3346 'name': pull_request.target_ref_parts.name,
3350 'name': pull_request.target_ref_parts.name,
3347 'type': pull_request.target_ref_parts.type,
3351 'type': pull_request.target_ref_parts.type,
3348 'commit_id': pull_request.target_ref_parts.commit_id,
3352 'commit_id': pull_request.target_ref_parts.commit_id,
3349 },
3353 },
3350 },
3354 },
3351 'merge': merge_data,
3355 'merge': merge_data,
3352 'author': pull_request.author.get_api_data(include_secrets=False,
3356 'author': pull_request.author.get_api_data(include_secrets=False,
3353 details='basic'),
3357 details='basic'),
3354 'reviewers': [
3358 'reviewers': [
3355 {
3359 {
3356 'user': reviewer.get_api_data(include_secrets=False,
3360 'user': reviewer.get_api_data(include_secrets=False,
3357 details='basic'),
3361 details='basic'),
3358 'reasons': reasons,
3362 'reasons': reasons,
3359 'review_status': st[0][1].status if st else 'not_reviewed',
3363 'review_status': st[0][1].status if st else 'not_reviewed',
3360 }
3364 }
3361 for reviewer, reasons, mandatory, st in
3365 for reviewer, reasons, mandatory, st in
3362 pull_request.reviewers_statuses()
3366 pull_request.reviewers_statuses()
3363 ]
3367 ]
3364 }
3368 }
3365
3369
3366 return data
3370 return data
3367
3371
3368
3372
3369 class PullRequest(Base, _PullRequestBase):
3373 class PullRequest(Base, _PullRequestBase):
3370 __tablename__ = 'pull_requests'
3374 __tablename__ = 'pull_requests'
3371 __table_args__ = (
3375 __table_args__ = (
3372 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3376 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3373 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3377 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3374 )
3378 )
3375
3379
3376 pull_request_id = Column(
3380 pull_request_id = Column(
3377 'pull_request_id', Integer(), nullable=False, primary_key=True)
3381 'pull_request_id', Integer(), nullable=False, primary_key=True)
3378
3382
3379 def __repr__(self):
3383 def __repr__(self):
3380 if self.pull_request_id:
3384 if self.pull_request_id:
3381 return '<DB:PullRequest #%s>' % self.pull_request_id
3385 return '<DB:PullRequest #%s>' % self.pull_request_id
3382 else:
3386 else:
3383 return '<DB:PullRequest at %#x>' % id(self)
3387 return '<DB:PullRequest at %#x>' % id(self)
3384
3388
3385 reviewers = relationship('PullRequestReviewers',
3389 reviewers = relationship('PullRequestReviewers',
3386 cascade="all, delete, delete-orphan")
3390 cascade="all, delete, delete-orphan")
3387 statuses = relationship('ChangesetStatus')
3391 statuses = relationship('ChangesetStatus')
3388 comments = relationship('ChangesetComment',
3392 comments = relationship('ChangesetComment',
3389 cascade="all, delete, delete-orphan")
3393 cascade="all, delete, delete-orphan")
3390 versions = relationship('PullRequestVersion',
3394 versions = relationship('PullRequestVersion',
3391 cascade="all, delete, delete-orphan",
3395 cascade="all, delete, delete-orphan",
3392 lazy='dynamic')
3396 lazy='dynamic')
3393
3397
3394 @classmethod
3398 @classmethod
3395 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3399 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3396 internal_methods=None):
3400 internal_methods=None):
3397
3401
3398 class PullRequestDisplay(object):
3402 class PullRequestDisplay(object):
3399 """
3403 """
3400 Special object wrapper for showing PullRequest data via Versions
3404 Special object wrapper for showing PullRequest data via Versions
3401 It mimics PR object as close as possible. This is read only object
3405 It mimics PR object as close as possible. This is read only object
3402 just for display
3406 just for display
3403 """
3407 """
3404
3408
3405 def __init__(self, attrs, internal=None):
3409 def __init__(self, attrs, internal=None):
3406 self.attrs = attrs
3410 self.attrs = attrs
3407 # internal have priority over the given ones via attrs
3411 # internal have priority over the given ones via attrs
3408 self.internal = internal or ['versions']
3412 self.internal = internal or ['versions']
3409
3413
3410 def __getattr__(self, item):
3414 def __getattr__(self, item):
3411 if item in self.internal:
3415 if item in self.internal:
3412 return getattr(self, item)
3416 return getattr(self, item)
3413 try:
3417 try:
3414 return self.attrs[item]
3418 return self.attrs[item]
3415 except KeyError:
3419 except KeyError:
3416 raise AttributeError(
3420 raise AttributeError(
3417 '%s object has no attribute %s' % (self, item))
3421 '%s object has no attribute %s' % (self, item))
3418
3422
3419 def __repr__(self):
3423 def __repr__(self):
3420 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3424 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3421
3425
3422 def versions(self):
3426 def versions(self):
3423 return pull_request_obj.versions.order_by(
3427 return pull_request_obj.versions.order_by(
3424 PullRequestVersion.pull_request_version_id).all()
3428 PullRequestVersion.pull_request_version_id).all()
3425
3429
3426 def is_closed(self):
3430 def is_closed(self):
3427 return pull_request_obj.is_closed()
3431 return pull_request_obj.is_closed()
3428
3432
3429 @property
3433 @property
3430 def pull_request_version_id(self):
3434 def pull_request_version_id(self):
3431 return getattr(pull_request_obj, 'pull_request_version_id', None)
3435 return getattr(pull_request_obj, 'pull_request_version_id', None)
3432
3436
3433 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3437 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3434
3438
3435 attrs.author = StrictAttributeDict(
3439 attrs.author = StrictAttributeDict(
3436 pull_request_obj.author.get_api_data())
3440 pull_request_obj.author.get_api_data())
3437 if pull_request_obj.target_repo:
3441 if pull_request_obj.target_repo:
3438 attrs.target_repo = StrictAttributeDict(
3442 attrs.target_repo = StrictAttributeDict(
3439 pull_request_obj.target_repo.get_api_data())
3443 pull_request_obj.target_repo.get_api_data())
3440 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3444 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3441
3445
3442 if pull_request_obj.source_repo:
3446 if pull_request_obj.source_repo:
3443 attrs.source_repo = StrictAttributeDict(
3447 attrs.source_repo = StrictAttributeDict(
3444 pull_request_obj.source_repo.get_api_data())
3448 pull_request_obj.source_repo.get_api_data())
3445 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3449 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3446
3450
3447 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3451 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3448 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3452 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3449 attrs.revisions = pull_request_obj.revisions
3453 attrs.revisions = pull_request_obj.revisions
3450
3454
3451 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3455 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3452 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3456 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3453 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3457 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3454
3458
3455 return PullRequestDisplay(attrs, internal=internal_methods)
3459 return PullRequestDisplay(attrs, internal=internal_methods)
3456
3460
3457 def is_closed(self):
3461 def is_closed(self):
3458 return self.status == self.STATUS_CLOSED
3462 return self.status == self.STATUS_CLOSED
3459
3463
3460 def __json__(self):
3464 def __json__(self):
3461 return {
3465 return {
3462 'revisions': self.revisions,
3466 'revisions': self.revisions,
3463 }
3467 }
3464
3468
3465 def calculated_review_status(self):
3469 def calculated_review_status(self):
3466 from rhodecode.model.changeset_status import ChangesetStatusModel
3470 from rhodecode.model.changeset_status import ChangesetStatusModel
3467 return ChangesetStatusModel().calculated_review_status(self)
3471 return ChangesetStatusModel().calculated_review_status(self)
3468
3472
3469 def reviewers_statuses(self):
3473 def reviewers_statuses(self):
3470 from rhodecode.model.changeset_status import ChangesetStatusModel
3474 from rhodecode.model.changeset_status import ChangesetStatusModel
3471 return ChangesetStatusModel().reviewers_statuses(self)
3475 return ChangesetStatusModel().reviewers_statuses(self)
3472
3476
3473 @property
3477 @property
3474 def workspace_id(self):
3478 def workspace_id(self):
3475 from rhodecode.model.pull_request import PullRequestModel
3479 from rhodecode.model.pull_request import PullRequestModel
3476 return PullRequestModel()._workspace_id(self)
3480 return PullRequestModel()._workspace_id(self)
3477
3481
3478 def get_shadow_repo(self):
3482 def get_shadow_repo(self):
3479 workspace_id = self.workspace_id
3483 workspace_id = self.workspace_id
3480 vcs_obj = self.target_repo.scm_instance()
3484 vcs_obj = self.target_repo.scm_instance()
3481 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3485 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3482 workspace_id)
3486 workspace_id)
3483 return vcs_obj._get_shadow_instance(shadow_repository_path)
3487 return vcs_obj._get_shadow_instance(shadow_repository_path)
3484
3488
3485
3489
3486 class PullRequestVersion(Base, _PullRequestBase):
3490 class PullRequestVersion(Base, _PullRequestBase):
3487 __tablename__ = 'pull_request_versions'
3491 __tablename__ = 'pull_request_versions'
3488 __table_args__ = (
3492 __table_args__ = (
3489 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3493 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3490 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3494 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3491 )
3495 )
3492
3496
3493 pull_request_version_id = Column(
3497 pull_request_version_id = Column(
3494 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3498 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3495 pull_request_id = Column(
3499 pull_request_id = Column(
3496 'pull_request_id', Integer(),
3500 'pull_request_id', Integer(),
3497 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3501 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3498 pull_request = relationship('PullRequest')
3502 pull_request = relationship('PullRequest')
3499
3503
3500 def __repr__(self):
3504 def __repr__(self):
3501 if self.pull_request_version_id:
3505 if self.pull_request_version_id:
3502 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3506 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3503 else:
3507 else:
3504 return '<DB:PullRequestVersion at %#x>' % id(self)
3508 return '<DB:PullRequestVersion at %#x>' % id(self)
3505
3509
3506 @property
3510 @property
3507 def reviewers(self):
3511 def reviewers(self):
3508 return self.pull_request.reviewers
3512 return self.pull_request.reviewers
3509
3513
3510 @property
3514 @property
3511 def versions(self):
3515 def versions(self):
3512 return self.pull_request.versions
3516 return self.pull_request.versions
3513
3517
3514 def is_closed(self):
3518 def is_closed(self):
3515 # calculate from original
3519 # calculate from original
3516 return self.pull_request.status == self.STATUS_CLOSED
3520 return self.pull_request.status == self.STATUS_CLOSED
3517
3521
3518 def calculated_review_status(self):
3522 def calculated_review_status(self):
3519 return self.pull_request.calculated_review_status()
3523 return self.pull_request.calculated_review_status()
3520
3524
3521 def reviewers_statuses(self):
3525 def reviewers_statuses(self):
3522 return self.pull_request.reviewers_statuses()
3526 return self.pull_request.reviewers_statuses()
3523
3527
3524
3528
3525 class PullRequestReviewers(Base, BaseModel):
3529 class PullRequestReviewers(Base, BaseModel):
3526 __tablename__ = 'pull_request_reviewers'
3530 __tablename__ = 'pull_request_reviewers'
3527 __table_args__ = (
3531 __table_args__ = (
3528 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3529 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3530 )
3534 )
3531
3535
3532 @hybrid_property
3536 @hybrid_property
3533 def reasons(self):
3537 def reasons(self):
3534 if not self._reasons:
3538 if not self._reasons:
3535 return []
3539 return []
3536 return self._reasons
3540 return self._reasons
3537
3541
3538 @reasons.setter
3542 @reasons.setter
3539 def reasons(self, val):
3543 def reasons(self, val):
3540 val = val or []
3544 val = val or []
3541 if any(not isinstance(x, basestring) for x in val):
3545 if any(not isinstance(x, basestring) for x in val):
3542 raise Exception('invalid reasons type, must be list of strings')
3546 raise Exception('invalid reasons type, must be list of strings')
3543 self._reasons = val
3547 self._reasons = val
3544
3548
3545 pull_requests_reviewers_id = Column(
3549 pull_requests_reviewers_id = Column(
3546 'pull_requests_reviewers_id', Integer(), nullable=False,
3550 'pull_requests_reviewers_id', Integer(), nullable=False,
3547 primary_key=True)
3551 primary_key=True)
3548 pull_request_id = Column(
3552 pull_request_id = Column(
3549 "pull_request_id", Integer(),
3553 "pull_request_id", Integer(),
3550 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3554 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3551 user_id = Column(
3555 user_id = Column(
3552 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3556 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3553 _reasons = Column(
3557 _reasons = Column(
3554 'reason', MutationList.as_mutable(
3558 'reason', MutationList.as_mutable(
3555 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3559 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3556 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3560 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3557 user = relationship('User')
3561 user = relationship('User')
3558 pull_request = relationship('PullRequest')
3562 pull_request = relationship('PullRequest')
3559
3563
3560
3564
3561 class Notification(Base, BaseModel):
3565 class Notification(Base, BaseModel):
3562 __tablename__ = 'notifications'
3566 __tablename__ = 'notifications'
3563 __table_args__ = (
3567 __table_args__ = (
3564 Index('notification_type_idx', 'type'),
3568 Index('notification_type_idx', 'type'),
3565 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3569 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3566 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3570 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3567 )
3571 )
3568
3572
3569 TYPE_CHANGESET_COMMENT = u'cs_comment'
3573 TYPE_CHANGESET_COMMENT = u'cs_comment'
3570 TYPE_MESSAGE = u'message'
3574 TYPE_MESSAGE = u'message'
3571 TYPE_MENTION = u'mention'
3575 TYPE_MENTION = u'mention'
3572 TYPE_REGISTRATION = u'registration'
3576 TYPE_REGISTRATION = u'registration'
3573 TYPE_PULL_REQUEST = u'pull_request'
3577 TYPE_PULL_REQUEST = u'pull_request'
3574 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3578 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3575
3579
3576 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3580 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3577 subject = Column('subject', Unicode(512), nullable=True)
3581 subject = Column('subject', Unicode(512), nullable=True)
3578 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3582 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3579 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3583 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3580 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3584 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3581 type_ = Column('type', Unicode(255))
3585 type_ = Column('type', Unicode(255))
3582
3586
3583 created_by_user = relationship('User')
3587 created_by_user = relationship('User')
3584 notifications_to_users = relationship('UserNotification', lazy='joined',
3588 notifications_to_users = relationship('UserNotification', lazy='joined',
3585 cascade="all, delete, delete-orphan")
3589 cascade="all, delete, delete-orphan")
3586
3590
3587 @property
3591 @property
3588 def recipients(self):
3592 def recipients(self):
3589 return [x.user for x in UserNotification.query()\
3593 return [x.user for x in UserNotification.query()\
3590 .filter(UserNotification.notification == self)\
3594 .filter(UserNotification.notification == self)\
3591 .order_by(UserNotification.user_id.asc()).all()]
3595 .order_by(UserNotification.user_id.asc()).all()]
3592
3596
3593 @classmethod
3597 @classmethod
3594 def create(cls, created_by, subject, body, recipients, type_=None):
3598 def create(cls, created_by, subject, body, recipients, type_=None):
3595 if type_ is None:
3599 if type_ is None:
3596 type_ = Notification.TYPE_MESSAGE
3600 type_ = Notification.TYPE_MESSAGE
3597
3601
3598 notification = cls()
3602 notification = cls()
3599 notification.created_by_user = created_by
3603 notification.created_by_user = created_by
3600 notification.subject = subject
3604 notification.subject = subject
3601 notification.body = body
3605 notification.body = body
3602 notification.type_ = type_
3606 notification.type_ = type_
3603 notification.created_on = datetime.datetime.now()
3607 notification.created_on = datetime.datetime.now()
3604
3608
3605 for u in recipients:
3609 for u in recipients:
3606 assoc = UserNotification()
3610 assoc = UserNotification()
3607 assoc.notification = notification
3611 assoc.notification = notification
3608
3612
3609 # if created_by is inside recipients mark his notification
3613 # if created_by is inside recipients mark his notification
3610 # as read
3614 # as read
3611 if u.user_id == created_by.user_id:
3615 if u.user_id == created_by.user_id:
3612 assoc.read = True
3616 assoc.read = True
3613
3617
3614 u.notifications.append(assoc)
3618 u.notifications.append(assoc)
3615 Session().add(notification)
3619 Session().add(notification)
3616
3620
3617 return notification
3621 return notification
3618
3622
3619 @property
3623 @property
3620 def description(self):
3624 def description(self):
3621 from rhodecode.model.notification import NotificationModel
3625 from rhodecode.model.notification import NotificationModel
3622 return NotificationModel().make_description(self)
3626 return NotificationModel().make_description(self)
3623
3627
3624
3628
3625 class UserNotification(Base, BaseModel):
3629 class UserNotification(Base, BaseModel):
3626 __tablename__ = 'user_to_notification'
3630 __tablename__ = 'user_to_notification'
3627 __table_args__ = (
3631 __table_args__ = (
3628 UniqueConstraint('user_id', 'notification_id'),
3632 UniqueConstraint('user_id', 'notification_id'),
3629 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3633 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3630 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3634 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3631 )
3635 )
3632 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3636 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3633 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3637 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3634 read = Column('read', Boolean, default=False)
3638 read = Column('read', Boolean, default=False)
3635 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3639 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3636
3640
3637 user = relationship('User', lazy="joined")
3641 user = relationship('User', lazy="joined")
3638 notification = relationship('Notification', lazy="joined",
3642 notification = relationship('Notification', lazy="joined",
3639 order_by=lambda: Notification.created_on.desc(),)
3643 order_by=lambda: Notification.created_on.desc(),)
3640
3644
3641 def mark_as_read(self):
3645 def mark_as_read(self):
3642 self.read = True
3646 self.read = True
3643 Session().add(self)
3647 Session().add(self)
3644
3648
3645
3649
3646 class Gist(Base, BaseModel):
3650 class Gist(Base, BaseModel):
3647 __tablename__ = 'gists'
3651 __tablename__ = 'gists'
3648 __table_args__ = (
3652 __table_args__ = (
3649 Index('g_gist_access_id_idx', 'gist_access_id'),
3653 Index('g_gist_access_id_idx', 'gist_access_id'),
3650 Index('g_created_on_idx', 'created_on'),
3654 Index('g_created_on_idx', 'created_on'),
3651 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3655 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3652 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3656 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3653 )
3657 )
3654 GIST_PUBLIC = u'public'
3658 GIST_PUBLIC = u'public'
3655 GIST_PRIVATE = u'private'
3659 GIST_PRIVATE = u'private'
3656 DEFAULT_FILENAME = u'gistfile1.txt'
3660 DEFAULT_FILENAME = u'gistfile1.txt'
3657
3661
3658 ACL_LEVEL_PUBLIC = u'acl_public'
3662 ACL_LEVEL_PUBLIC = u'acl_public'
3659 ACL_LEVEL_PRIVATE = u'acl_private'
3663 ACL_LEVEL_PRIVATE = u'acl_private'
3660
3664
3661 gist_id = Column('gist_id', Integer(), primary_key=True)
3665 gist_id = Column('gist_id', Integer(), primary_key=True)
3662 gist_access_id = Column('gist_access_id', Unicode(250))
3666 gist_access_id = Column('gist_access_id', Unicode(250))
3663 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3667 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3664 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3668 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3665 gist_expires = Column('gist_expires', Float(53), nullable=False)
3669 gist_expires = Column('gist_expires', Float(53), nullable=False)
3666 gist_type = Column('gist_type', Unicode(128), nullable=False)
3670 gist_type = Column('gist_type', Unicode(128), nullable=False)
3667 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3671 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3668 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3672 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3669 acl_level = Column('acl_level', Unicode(128), nullable=True)
3673 acl_level = Column('acl_level', Unicode(128), nullable=True)
3670
3674
3671 owner = relationship('User')
3675 owner = relationship('User')
3672
3676
3673 def __repr__(self):
3677 def __repr__(self):
3674 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3678 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3675
3679
3676 @classmethod
3680 @classmethod
3677 def get_or_404(cls, id_, pyramid_exc=False):
3681 def get_or_404(cls, id_, pyramid_exc=False):
3678
3682
3679 if pyramid_exc:
3683 if pyramid_exc:
3680 from pyramid.httpexceptions import HTTPNotFound
3684 from pyramid.httpexceptions import HTTPNotFound
3681 else:
3685 else:
3682 from webob.exc import HTTPNotFound
3686 from webob.exc import HTTPNotFound
3683
3687
3684 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3688 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3685 if not res:
3689 if not res:
3686 raise HTTPNotFound
3690 raise HTTPNotFound
3687 return res
3691 return res
3688
3692
3689 @classmethod
3693 @classmethod
3690 def get_by_access_id(cls, gist_access_id):
3694 def get_by_access_id(cls, gist_access_id):
3691 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3695 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3692
3696
3693 def gist_url(self):
3697 def gist_url(self):
3694 import rhodecode
3698 import rhodecode
3695 from pylons import url
3699 from pylons import url
3696
3700
3697 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3701 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3698 if alias_url:
3702 if alias_url:
3699 return alias_url.replace('{gistid}', self.gist_access_id)
3703 return alias_url.replace('{gistid}', self.gist_access_id)
3700
3704
3701 return url('gist', gist_id=self.gist_access_id, qualified=True)
3705 return url('gist', gist_id=self.gist_access_id, qualified=True)
3702
3706
3703 @classmethod
3707 @classmethod
3704 def base_path(cls):
3708 def base_path(cls):
3705 """
3709 """
3706 Returns base path when all gists are stored
3710 Returns base path when all gists are stored
3707
3711
3708 :param cls:
3712 :param cls:
3709 """
3713 """
3710 from rhodecode.model.gist import GIST_STORE_LOC
3714 from rhodecode.model.gist import GIST_STORE_LOC
3711 q = Session().query(RhodeCodeUi)\
3715 q = Session().query(RhodeCodeUi)\
3712 .filter(RhodeCodeUi.ui_key == URL_SEP)
3716 .filter(RhodeCodeUi.ui_key == URL_SEP)
3713 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3717 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3714 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3718 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3715
3719
3716 def get_api_data(self):
3720 def get_api_data(self):
3717 """
3721 """
3718 Common function for generating gist related data for API
3722 Common function for generating gist related data for API
3719 """
3723 """
3720 gist = self
3724 gist = self
3721 data = {
3725 data = {
3722 'gist_id': gist.gist_id,
3726 'gist_id': gist.gist_id,
3723 'type': gist.gist_type,
3727 'type': gist.gist_type,
3724 'access_id': gist.gist_access_id,
3728 'access_id': gist.gist_access_id,
3725 'description': gist.gist_description,
3729 'description': gist.gist_description,
3726 'url': gist.gist_url(),
3730 'url': gist.gist_url(),
3727 'expires': gist.gist_expires,
3731 'expires': gist.gist_expires,
3728 'created_on': gist.created_on,
3732 'created_on': gist.created_on,
3729 'modified_at': gist.modified_at,
3733 'modified_at': gist.modified_at,
3730 'content': None,
3734 'content': None,
3731 'acl_level': gist.acl_level,
3735 'acl_level': gist.acl_level,
3732 }
3736 }
3733 return data
3737 return data
3734
3738
3735 def __json__(self):
3739 def __json__(self):
3736 data = dict(
3740 data = dict(
3737 )
3741 )
3738 data.update(self.get_api_data())
3742 data.update(self.get_api_data())
3739 return data
3743 return data
3740 # SCM functions
3744 # SCM functions
3741
3745
3742 def scm_instance(self, **kwargs):
3746 def scm_instance(self, **kwargs):
3743 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3747 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3744 return get_vcs_instance(
3748 return get_vcs_instance(
3745 repo_path=safe_str(full_repo_path), create=False)
3749 repo_path=safe_str(full_repo_path), create=False)
3746
3750
3747
3751
3748 class ExternalIdentity(Base, BaseModel):
3752 class ExternalIdentity(Base, BaseModel):
3749 __tablename__ = 'external_identities'
3753 __tablename__ = 'external_identities'
3750 __table_args__ = (
3754 __table_args__ = (
3751 Index('local_user_id_idx', 'local_user_id'),
3755 Index('local_user_id_idx', 'local_user_id'),
3752 Index('external_id_idx', 'external_id'),
3756 Index('external_id_idx', 'external_id'),
3753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3757 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3754 'mysql_charset': 'utf8'})
3758 'mysql_charset': 'utf8'})
3755
3759
3756 external_id = Column('external_id', Unicode(255), default=u'',
3760 external_id = Column('external_id', Unicode(255), default=u'',
3757 primary_key=True)
3761 primary_key=True)
3758 external_username = Column('external_username', Unicode(1024), default=u'')
3762 external_username = Column('external_username', Unicode(1024), default=u'')
3759 local_user_id = Column('local_user_id', Integer(),
3763 local_user_id = Column('local_user_id', Integer(),
3760 ForeignKey('users.user_id'), primary_key=True)
3764 ForeignKey('users.user_id'), primary_key=True)
3761 provider_name = Column('provider_name', Unicode(255), default=u'',
3765 provider_name = Column('provider_name', Unicode(255), default=u'',
3762 primary_key=True)
3766 primary_key=True)
3763 access_token = Column('access_token', String(1024), default=u'')
3767 access_token = Column('access_token', String(1024), default=u'')
3764 alt_token = Column('alt_token', String(1024), default=u'')
3768 alt_token = Column('alt_token', String(1024), default=u'')
3765 token_secret = Column('token_secret', String(1024), default=u'')
3769 token_secret = Column('token_secret', String(1024), default=u'')
3766
3770
3767 @classmethod
3771 @classmethod
3768 def by_external_id_and_provider(cls, external_id, provider_name,
3772 def by_external_id_and_provider(cls, external_id, provider_name,
3769 local_user_id=None):
3773 local_user_id=None):
3770 """
3774 """
3771 Returns ExternalIdentity instance based on search params
3775 Returns ExternalIdentity instance based on search params
3772
3776
3773 :param external_id:
3777 :param external_id:
3774 :param provider_name:
3778 :param provider_name:
3775 :return: ExternalIdentity
3779 :return: ExternalIdentity
3776 """
3780 """
3777 query = cls.query()
3781 query = cls.query()
3778 query = query.filter(cls.external_id == external_id)
3782 query = query.filter(cls.external_id == external_id)
3779 query = query.filter(cls.provider_name == provider_name)
3783 query = query.filter(cls.provider_name == provider_name)
3780 if local_user_id:
3784 if local_user_id:
3781 query = query.filter(cls.local_user_id == local_user_id)
3785 query = query.filter(cls.local_user_id == local_user_id)
3782 return query.first()
3786 return query.first()
3783
3787
3784 @classmethod
3788 @classmethod
3785 def user_by_external_id_and_provider(cls, external_id, provider_name):
3789 def user_by_external_id_and_provider(cls, external_id, provider_name):
3786 """
3790 """
3787 Returns User instance based on search params
3791 Returns User instance based on search params
3788
3792
3789 :param external_id:
3793 :param external_id:
3790 :param provider_name:
3794 :param provider_name:
3791 :return: User
3795 :return: User
3792 """
3796 """
3793 query = User.query()
3797 query = User.query()
3794 query = query.filter(cls.external_id == external_id)
3798 query = query.filter(cls.external_id == external_id)
3795 query = query.filter(cls.provider_name == provider_name)
3799 query = query.filter(cls.provider_name == provider_name)
3796 query = query.filter(User.user_id == cls.local_user_id)
3800 query = query.filter(User.user_id == cls.local_user_id)
3797 return query.first()
3801 return query.first()
3798
3802
3799 @classmethod
3803 @classmethod
3800 def by_local_user_id(cls, local_user_id):
3804 def by_local_user_id(cls, local_user_id):
3801 """
3805 """
3802 Returns all tokens for user
3806 Returns all tokens for user
3803
3807
3804 :param local_user_id:
3808 :param local_user_id:
3805 :return: ExternalIdentity
3809 :return: ExternalIdentity
3806 """
3810 """
3807 query = cls.query()
3811 query = cls.query()
3808 query = query.filter(cls.local_user_id == local_user_id)
3812 query = query.filter(cls.local_user_id == local_user_id)
3809 return query
3813 return query
3810
3814
3811
3815
3812 class Integration(Base, BaseModel):
3816 class Integration(Base, BaseModel):
3813 __tablename__ = 'integrations'
3817 __tablename__ = 'integrations'
3814 __table_args__ = (
3818 __table_args__ = (
3815 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3819 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3816 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3820 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3817 )
3821 )
3818
3822
3819 integration_id = Column('integration_id', Integer(), primary_key=True)
3823 integration_id = Column('integration_id', Integer(), primary_key=True)
3820 integration_type = Column('integration_type', String(255))
3824 integration_type = Column('integration_type', String(255))
3821 enabled = Column('enabled', Boolean(), nullable=False)
3825 enabled = Column('enabled', Boolean(), nullable=False)
3822 name = Column('name', String(255), nullable=False)
3826 name = Column('name', String(255), nullable=False)
3823 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3827 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3824 default=False)
3828 default=False)
3825
3829
3826 settings = Column(
3830 settings = Column(
3827 'settings_json', MutationObj.as_mutable(
3831 'settings_json', MutationObj.as_mutable(
3828 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3832 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3829 repo_id = Column(
3833 repo_id = Column(
3830 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3834 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3831 nullable=True, unique=None, default=None)
3835 nullable=True, unique=None, default=None)
3832 repo = relationship('Repository', lazy='joined')
3836 repo = relationship('Repository', lazy='joined')
3833
3837
3834 repo_group_id = Column(
3838 repo_group_id = Column(
3835 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3839 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3836 nullable=True, unique=None, default=None)
3840 nullable=True, unique=None, default=None)
3837 repo_group = relationship('RepoGroup', lazy='joined')
3841 repo_group = relationship('RepoGroup', lazy='joined')
3838
3842
3839 @property
3843 @property
3840 def scope(self):
3844 def scope(self):
3841 if self.repo:
3845 if self.repo:
3842 return repr(self.repo)
3846 return repr(self.repo)
3843 if self.repo_group:
3847 if self.repo_group:
3844 if self.child_repos_only:
3848 if self.child_repos_only:
3845 return repr(self.repo_group) + ' (child repos only)'
3849 return repr(self.repo_group) + ' (child repos only)'
3846 else:
3850 else:
3847 return repr(self.repo_group) + ' (recursive)'
3851 return repr(self.repo_group) + ' (recursive)'
3848 if self.child_repos_only:
3852 if self.child_repos_only:
3849 return 'root_repos'
3853 return 'root_repos'
3850 return 'global'
3854 return 'global'
3851
3855
3852 def __repr__(self):
3856 def __repr__(self):
3853 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3857 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3854
3858
3855
3859
3856 class RepoReviewRuleUser(Base, BaseModel):
3860 class RepoReviewRuleUser(Base, BaseModel):
3857 __tablename__ = 'repo_review_rules_users'
3861 __tablename__ = 'repo_review_rules_users'
3858 __table_args__ = (
3862 __table_args__ = (
3859 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3863 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3860 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3864 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3861 )
3865 )
3862 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3866 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3863 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3867 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3864 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3868 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3865 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3869 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3866 user = relationship('User')
3870 user = relationship('User')
3867
3871
3868 def rule_data(self):
3872 def rule_data(self):
3869 return {
3873 return {
3870 'mandatory': self.mandatory
3874 'mandatory': self.mandatory
3871 }
3875 }
3872
3876
3873
3877
3874 class RepoReviewRuleUserGroup(Base, BaseModel):
3878 class RepoReviewRuleUserGroup(Base, BaseModel):
3875 __tablename__ = 'repo_review_rules_users_groups'
3879 __tablename__ = 'repo_review_rules_users_groups'
3876 __table_args__ = (
3880 __table_args__ = (
3877 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3881 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3878 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3882 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3879 )
3883 )
3880 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3884 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3881 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3885 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3882 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3886 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3883 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3887 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3884 users_group = relationship('UserGroup')
3888 users_group = relationship('UserGroup')
3885
3889
3886 def rule_data(self):
3890 def rule_data(self):
3887 return {
3891 return {
3888 'mandatory': self.mandatory
3892 'mandatory': self.mandatory
3889 }
3893 }
3890
3894
3891
3895
3892 class RepoReviewRule(Base, BaseModel):
3896 class RepoReviewRule(Base, BaseModel):
3893 __tablename__ = 'repo_review_rules'
3897 __tablename__ = 'repo_review_rules'
3894 __table_args__ = (
3898 __table_args__ = (
3895 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3899 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3896 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3900 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3897 )
3901 )
3898
3902
3899 repo_review_rule_id = Column(
3903 repo_review_rule_id = Column(
3900 'repo_review_rule_id', Integer(), primary_key=True)
3904 'repo_review_rule_id', Integer(), primary_key=True)
3901 repo_id = Column(
3905 repo_id = Column(
3902 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3906 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3903 repo = relationship('Repository', backref='review_rules')
3907 repo = relationship('Repository', backref='review_rules')
3904
3908
3905 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3909 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3906 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3910 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3907
3911
3908 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
3912 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
3909 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
3913 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
3910 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
3914 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
3911 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
3915 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
3912
3916
3913 rule_users = relationship('RepoReviewRuleUser')
3917 rule_users = relationship('RepoReviewRuleUser')
3914 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3918 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3915
3919
3916 @hybrid_property
3920 @hybrid_property
3917 def branch_pattern(self):
3921 def branch_pattern(self):
3918 return self._branch_pattern or '*'
3922 return self._branch_pattern or '*'
3919
3923
3920 def _validate_glob(self, value):
3924 def _validate_glob(self, value):
3921 re.compile('^' + glob2re(value) + '$')
3925 re.compile('^' + glob2re(value) + '$')
3922
3926
3923 @branch_pattern.setter
3927 @branch_pattern.setter
3924 def branch_pattern(self, value):
3928 def branch_pattern(self, value):
3925 self._validate_glob(value)
3929 self._validate_glob(value)
3926 self._branch_pattern = value or '*'
3930 self._branch_pattern = value or '*'
3927
3931
3928 @hybrid_property
3932 @hybrid_property
3929 def file_pattern(self):
3933 def file_pattern(self):
3930 return self._file_pattern or '*'
3934 return self._file_pattern or '*'
3931
3935
3932 @file_pattern.setter
3936 @file_pattern.setter
3933 def file_pattern(self, value):
3937 def file_pattern(self, value):
3934 self._validate_glob(value)
3938 self._validate_glob(value)
3935 self._file_pattern = value or '*'
3939 self._file_pattern = value or '*'
3936
3940
3937 def matches(self, branch, files_changed):
3941 def matches(self, branch, files_changed):
3938 """
3942 """
3939 Check if this review rule matches a branch/files in a pull request
3943 Check if this review rule matches a branch/files in a pull request
3940
3944
3941 :param branch: branch name for the commit
3945 :param branch: branch name for the commit
3942 :param files_changed: list of file paths changed in the pull request
3946 :param files_changed: list of file paths changed in the pull request
3943 """
3947 """
3944
3948
3945 branch = branch or ''
3949 branch = branch or ''
3946 files_changed = files_changed or []
3950 files_changed = files_changed or []
3947
3951
3948 branch_matches = True
3952 branch_matches = True
3949 if branch:
3953 if branch:
3950 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3954 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3951 branch_matches = bool(branch_regex.search(branch))
3955 branch_matches = bool(branch_regex.search(branch))
3952
3956
3953 files_matches = True
3957 files_matches = True
3954 if self.file_pattern != '*':
3958 if self.file_pattern != '*':
3955 files_matches = False
3959 files_matches = False
3956 file_regex = re.compile(glob2re(self.file_pattern))
3960 file_regex = re.compile(glob2re(self.file_pattern))
3957 for filename in files_changed:
3961 for filename in files_changed:
3958 if file_regex.search(filename):
3962 if file_regex.search(filename):
3959 files_matches = True
3963 files_matches = True
3960 break
3964 break
3961
3965
3962 return branch_matches and files_matches
3966 return branch_matches and files_matches
3963
3967
3964 @property
3968 @property
3965 def review_users(self):
3969 def review_users(self):
3966 """ Returns the users which this rule applies to """
3970 """ Returns the users which this rule applies to """
3967
3971
3968 users = collections.OrderedDict()
3972 users = collections.OrderedDict()
3969
3973
3970 for rule_user in self.rule_users:
3974 for rule_user in self.rule_users:
3971 if rule_user.user.active:
3975 if rule_user.user.active:
3972 if rule_user.user not in users:
3976 if rule_user.user not in users:
3973 users[rule_user.user.username] = {
3977 users[rule_user.user.username] = {
3974 'user': rule_user.user,
3978 'user': rule_user.user,
3975 'source': 'user',
3979 'source': 'user',
3976 'source_data': {},
3980 'source_data': {},
3977 'data': rule_user.rule_data()
3981 'data': rule_user.rule_data()
3978 }
3982 }
3979
3983
3980 for rule_user_group in self.rule_user_groups:
3984 for rule_user_group in self.rule_user_groups:
3981 source_data = {
3985 source_data = {
3982 'name': rule_user_group.users_group.users_group_name,
3986 'name': rule_user_group.users_group.users_group_name,
3983 'members': len(rule_user_group.users_group.members)
3987 'members': len(rule_user_group.users_group.members)
3984 }
3988 }
3985 for member in rule_user_group.users_group.members:
3989 for member in rule_user_group.users_group.members:
3986 if member.user.active:
3990 if member.user.active:
3987 users[member.user.username] = {
3991 users[member.user.username] = {
3988 'user': member.user,
3992 'user': member.user,
3989 'source': 'user_group',
3993 'source': 'user_group',
3990 'source_data': source_data,
3994 'source_data': source_data,
3991 'data': rule_user_group.rule_data()
3995 'data': rule_user_group.rule_data()
3992 }
3996 }
3993
3997
3994 return users
3998 return users
3995
3999
3996 def __repr__(self):
4000 def __repr__(self):
3997 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4001 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3998 self.repo_review_rule_id, self.repo)
4002 self.repo_review_rule_id, self.repo)
3999
4003
4000
4004
4001 class DbMigrateVersion(Base, BaseModel):
4005 class DbMigrateVersion(Base, BaseModel):
4002 __tablename__ = 'db_migrate_version'
4006 __tablename__ = 'db_migrate_version'
4003 __table_args__ = (
4007 __table_args__ = (
4004 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4008 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4005 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4009 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4006 )
4010 )
4007 repository_id = Column('repository_id', String(250), primary_key=True)
4011 repository_id = Column('repository_id', String(250), primary_key=True)
4008 repository_path = Column('repository_path', Text)
4012 repository_path = Column('repository_path', Text)
4009 version = Column('version', Integer)
4013 version = Column('version', Integer)
4010
4014
4011
4015
4012 class DbSession(Base, BaseModel):
4016 class DbSession(Base, BaseModel):
4013 __tablename__ = 'db_session'
4017 __tablename__ = 'db_session'
4014 __table_args__ = (
4018 __table_args__ = (
4015 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4019 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4016 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4020 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4017 )
4021 )
4018
4022
4019 def __repr__(self):
4023 def __repr__(self):
4020 return '<DB:DbSession({})>'.format(self.id)
4024 return '<DB:DbSession({})>'.format(self.id)
4021
4025
4022 id = Column('id', Integer())
4026 id = Column('id', Integer())
4023 namespace = Column('namespace', String(255), primary_key=True)
4027 namespace = Column('namespace', String(255), primary_key=True)
4024 accessed = Column('accessed', DateTime, nullable=False)
4028 accessed = Column('accessed', DateTime, nullable=False)
4025 created = Column('created', DateTime, nullable=False)
4029 created = Column('created', DateTime, nullable=False)
4026 data = Column('data', PickleType, nullable=False)
4030 data = Column('data', PickleType, nullable=False)
@@ -1,1504 +1,1524 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35 from sqlalchemy import or_
35 from sqlalchemy import or_
36
36
37 from rhodecode import events
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
40 from rhodecode.lib.markup_renderer import (
41 from rhodecode.lib.markup_renderer import (
41 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
42 from rhodecode.lib.utils import action_logger
43 from rhodecode.lib.utils import action_logger
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.exceptions import (
48 from rhodecode.lib.vcs.exceptions import (
48 CommitDoesNotExistError, EmptyRepositoryError)
49 CommitDoesNotExistError, EmptyRepositoryError)
49 from rhodecode.model import BaseModel
50 from rhodecode.model import BaseModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.db import (
53 from rhodecode.model.db import (
53 PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequestVersion, ChangesetComment, Repository)
55 PullRequestVersion, ChangesetComment, Repository)
55 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
56 from rhodecode.model.notification import NotificationModel, \
57 from rhodecode.model.notification import NotificationModel, \
57 EmailNotificationModel
58 EmailNotificationModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.settings import VcsSettingsModel
60 from rhodecode.model.settings import VcsSettingsModel
60
61
61
62
62 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
63
64
64
65
65 # Data structure to hold the response data when updating commits during a pull
66 # Data structure to hold the response data when updating commits during a pull
66 # request update.
67 # request update.
67 UpdateResponse = namedtuple('UpdateResponse', [
68 UpdateResponse = namedtuple('UpdateResponse', [
68 'executed', 'reason', 'new', 'old', 'changes',
69 'executed', 'reason', 'new', 'old', 'changes',
69 'source_changed', 'target_changed'])
70 'source_changed', 'target_changed'])
70
71
71
72
72 class PullRequestModel(BaseModel):
73 class PullRequestModel(BaseModel):
73
74
74 cls = PullRequest
75 cls = PullRequest
75
76
76 DIFF_CONTEXT = 3
77 DIFF_CONTEXT = 3
77
78
78 MERGE_STATUS_MESSAGES = {
79 MERGE_STATUS_MESSAGES = {
79 MergeFailureReason.NONE: lazy_ugettext(
80 MergeFailureReason.NONE: lazy_ugettext(
80 'This pull request can be automatically merged.'),
81 'This pull request can be automatically merged.'),
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 'This pull request cannot be merged because of an unhandled'
83 'This pull request cannot be merged because of an unhandled'
83 ' exception.'),
84 ' exception.'),
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 'This pull request cannot be merged because of merge conflicts.'),
86 'This pull request cannot be merged because of merge conflicts.'),
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 'This pull request could not be merged because push to target'
88 'This pull request could not be merged because push to target'
88 ' failed.'),
89 ' failed.'),
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 'This pull request cannot be merged because the target is not a'
91 'This pull request cannot be merged because the target is not a'
91 ' head.'),
92 ' head.'),
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 'This pull request cannot be merged because the source contains'
94 'This pull request cannot be merged because the source contains'
94 ' more branches than the target.'),
95 ' more branches than the target.'),
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 'This pull request cannot be merged because the target has'
97 'This pull request cannot be merged because the target has'
97 ' multiple heads.'),
98 ' multiple heads.'),
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 'This pull request cannot be merged because the target repository'
100 'This pull request cannot be merged because the target repository'
100 ' is locked.'),
101 ' is locked.'),
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 'This pull request cannot be merged because the target or the '
103 'This pull request cannot be merged because the target or the '
103 'source reference is missing.'),
104 'source reference is missing.'),
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 'This pull request cannot be merged because the target '
106 'This pull request cannot be merged because the target '
106 'reference is missing.'),
107 'reference is missing.'),
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 'This pull request cannot be merged because the source '
109 'This pull request cannot be merged because the source '
109 'reference is missing.'),
110 'reference is missing.'),
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 'This pull request cannot be merged because of conflicts related '
112 'This pull request cannot be merged because of conflicts related '
112 'to sub repositories.'),
113 'to sub repositories.'),
113 }
114 }
114
115
115 UPDATE_STATUS_MESSAGES = {
116 UPDATE_STATUS_MESSAGES = {
116 UpdateFailureReason.NONE: lazy_ugettext(
117 UpdateFailureReason.NONE: lazy_ugettext(
117 'Pull request update successful.'),
118 'Pull request update successful.'),
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 'Pull request update failed because of an unknown error.'),
120 'Pull request update failed because of an unknown error.'),
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 'No update needed because the source and target have not changed.'),
122 'No update needed because the source and target have not changed.'),
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 'Pull request cannot be updated because the reference type is '
124 'Pull request cannot be updated because the reference type is '
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 'This pull request cannot be updated because the target '
127 'This pull request cannot be updated because the target '
127 'reference is missing.'),
128 'reference is missing.'),
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 'This pull request cannot be updated because the source '
130 'This pull request cannot be updated because the source '
130 'reference is missing.'),
131 'reference is missing.'),
131 }
132 }
132
133
133 def __get_pull_request(self, pull_request):
134 def __get_pull_request(self, pull_request):
134 return self._get_instance((
135 return self._get_instance((
135 PullRequest, PullRequestVersion), pull_request)
136 PullRequest, PullRequestVersion), pull_request)
136
137
137 def _check_perms(self, perms, pull_request, user, api=False):
138 def _check_perms(self, perms, pull_request, user, api=False):
138 if not api:
139 if not api:
139 return h.HasRepoPermissionAny(*perms)(
140 return h.HasRepoPermissionAny(*perms)(
140 user=user, repo_name=pull_request.target_repo.repo_name)
141 user=user, repo_name=pull_request.target_repo.repo_name)
141 else:
142 else:
142 return h.HasRepoPermissionAnyApi(*perms)(
143 return h.HasRepoPermissionAnyApi(*perms)(
143 user=user, repo_name=pull_request.target_repo.repo_name)
144 user=user, repo_name=pull_request.target_repo.repo_name)
144
145
145 def check_user_read(self, pull_request, user, api=False):
146 def check_user_read(self, pull_request, user, api=False):
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 return self._check_perms(_perms, pull_request, user, api)
148 return self._check_perms(_perms, pull_request, user, api)
148
149
149 def check_user_merge(self, pull_request, user, api=False):
150 def check_user_merge(self, pull_request, user, api=False):
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 return self._check_perms(_perms, pull_request, user, api)
152 return self._check_perms(_perms, pull_request, user, api)
152
153
153 def check_user_update(self, pull_request, user, api=False):
154 def check_user_update(self, pull_request, user, api=False):
154 owner = user.user_id == pull_request.user_id
155 owner = user.user_id == pull_request.user_id
155 return self.check_user_merge(pull_request, user, api) or owner
156 return self.check_user_merge(pull_request, user, api) or owner
156
157
157 def check_user_delete(self, pull_request, user):
158 def check_user_delete(self, pull_request, user):
158 owner = user.user_id == pull_request.user_id
159 owner = user.user_id == pull_request.user_id
159 _perms = ('repository.admin',)
160 _perms = ('repository.admin',)
160 return self._check_perms(_perms, pull_request, user) or owner
161 return self._check_perms(_perms, pull_request, user) or owner
161
162
162 def check_user_change_status(self, pull_request, user, api=False):
163 def check_user_change_status(self, pull_request, user, api=False):
163 reviewer = user.user_id in [x.user_id for x in
164 reviewer = user.user_id in [x.user_id for x in
164 pull_request.reviewers]
165 pull_request.reviewers]
165 return self.check_user_update(pull_request, user, api) or reviewer
166 return self.check_user_update(pull_request, user, api) or reviewer
166
167
167 def get(self, pull_request):
168 def get(self, pull_request):
168 return self.__get_pull_request(pull_request)
169 return self.__get_pull_request(pull_request)
169
170
170 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
171 opened_by=None, order_by=None,
172 opened_by=None, order_by=None,
172 order_dir='desc'):
173 order_dir='desc'):
173 repo = None
174 repo = None
174 if repo_name:
175 if repo_name:
175 repo = self._get_repo(repo_name)
176 repo = self._get_repo(repo_name)
176
177
177 q = PullRequest.query()
178 q = PullRequest.query()
178
179
179 # source or target
180 # source or target
180 if repo and source:
181 if repo and source:
181 q = q.filter(PullRequest.source_repo == repo)
182 q = q.filter(PullRequest.source_repo == repo)
182 elif repo:
183 elif repo:
183 q = q.filter(PullRequest.target_repo == repo)
184 q = q.filter(PullRequest.target_repo == repo)
184
185
185 # closed,opened
186 # closed,opened
186 if statuses:
187 if statuses:
187 q = q.filter(PullRequest.status.in_(statuses))
188 q = q.filter(PullRequest.status.in_(statuses))
188
189
189 # opened by filter
190 # opened by filter
190 if opened_by:
191 if opened_by:
191 q = q.filter(PullRequest.user_id.in_(opened_by))
192 q = q.filter(PullRequest.user_id.in_(opened_by))
192
193
193 if order_by:
194 if order_by:
194 order_map = {
195 order_map = {
195 'name_raw': PullRequest.pull_request_id,
196 'name_raw': PullRequest.pull_request_id,
196 'title': PullRequest.title,
197 'title': PullRequest.title,
197 'updated_on_raw': PullRequest.updated_on,
198 'updated_on_raw': PullRequest.updated_on,
198 'target_repo': PullRequest.target_repo_id
199 'target_repo': PullRequest.target_repo_id
199 }
200 }
200 if order_dir == 'asc':
201 if order_dir == 'asc':
201 q = q.order_by(order_map[order_by].asc())
202 q = q.order_by(order_map[order_by].asc())
202 else:
203 else:
203 q = q.order_by(order_map[order_by].desc())
204 q = q.order_by(order_map[order_by].desc())
204
205
205 return q
206 return q
206
207
207 def count_all(self, repo_name, source=False, statuses=None,
208 def count_all(self, repo_name, source=False, statuses=None,
208 opened_by=None):
209 opened_by=None):
209 """
210 """
210 Count the number of pull requests for a specific repository.
211 Count the number of pull requests for a specific repository.
211
212
212 :param repo_name: target or source repo
213 :param repo_name: target or source repo
213 :param source: boolean flag to specify if repo_name refers to source
214 :param source: boolean flag to specify if repo_name refers to source
214 :param statuses: list of pull request statuses
215 :param statuses: list of pull request statuses
215 :param opened_by: author user of the pull request
216 :param opened_by: author user of the pull request
216 :returns: int number of pull requests
217 :returns: int number of pull requests
217 """
218 """
218 q = self._prepare_get_all_query(
219 q = self._prepare_get_all_query(
219 repo_name, source=source, statuses=statuses, opened_by=opened_by)
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
220
221
221 return q.count()
222 return q.count()
222
223
223 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
224 offset=0, length=None, order_by=None, order_dir='desc'):
225 offset=0, length=None, order_by=None, order_dir='desc'):
225 """
226 """
226 Get all pull requests for a specific repository.
227 Get all pull requests for a specific repository.
227
228
228 :param repo_name: target or source repo
229 :param repo_name: target or source repo
229 :param source: boolean flag to specify if repo_name refers to source
230 :param source: boolean flag to specify if repo_name refers to source
230 :param statuses: list of pull request statuses
231 :param statuses: list of pull request statuses
231 :param opened_by: author user of the pull request
232 :param opened_by: author user of the pull request
232 :param offset: pagination offset
233 :param offset: pagination offset
233 :param length: length of returned list
234 :param length: length of returned list
234 :param order_by: order of the returned list
235 :param order_by: order of the returned list
235 :param order_dir: 'asc' or 'desc' ordering direction
236 :param order_dir: 'asc' or 'desc' ordering direction
236 :returns: list of pull requests
237 :returns: list of pull requests
237 """
238 """
238 q = self._prepare_get_all_query(
239 q = self._prepare_get_all_query(
239 repo_name, source=source, statuses=statuses, opened_by=opened_by,
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
240 order_by=order_by, order_dir=order_dir)
241 order_by=order_by, order_dir=order_dir)
241
242
242 if length:
243 if length:
243 pull_requests = q.limit(length).offset(offset).all()
244 pull_requests = q.limit(length).offset(offset).all()
244 else:
245 else:
245 pull_requests = q.all()
246 pull_requests = q.all()
246
247
247 return pull_requests
248 return pull_requests
248
249
249 def count_awaiting_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
250 opened_by=None):
251 opened_by=None):
251 """
252 """
252 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
253 awaiting review.
254 awaiting review.
254
255
255 :param repo_name: target or source repo
256 :param repo_name: target or source repo
256 :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
257 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
258 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
259 :returns: int number of pull requests
260 :returns: int number of pull requests
260 """
261 """
261 pull_requests = self.get_awaiting_review(
262 pull_requests = self.get_awaiting_review(
262 repo_name, source=source, statuses=statuses, opened_by=opened_by)
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
263
264
264 return len(pull_requests)
265 return len(pull_requests)
265
266
266 def get_awaiting_review(self, repo_name, source=False, statuses=None,
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
267 opened_by=None, offset=0, length=None,
268 opened_by=None, offset=0, length=None,
268 order_by=None, order_dir='desc'):
269 order_by=None, order_dir='desc'):
269 """
270 """
270 Get all pull requests for a specific repository that are awaiting
271 Get all pull requests for a specific repository that are awaiting
271 review.
272 review.
272
273
273 :param repo_name: target or source repo
274 :param repo_name: target or source repo
274 :param source: boolean flag to specify if repo_name refers to source
275 :param source: boolean flag to specify if repo_name refers to source
275 :param statuses: list of pull request statuses
276 :param statuses: list of pull request statuses
276 :param opened_by: author user of the pull request
277 :param opened_by: author user of the pull request
277 :param offset: pagination offset
278 :param offset: pagination offset
278 :param length: length of returned list
279 :param length: length of returned list
279 :param order_by: order of the returned list
280 :param order_by: order of the returned list
280 :param order_dir: 'asc' or 'desc' ordering direction
281 :param order_dir: 'asc' or 'desc' ordering direction
281 :returns: list of pull requests
282 :returns: list of pull requests
282 """
283 """
283 pull_requests = self.get_all(
284 pull_requests = self.get_all(
284 repo_name, source=source, statuses=statuses, opened_by=opened_by,
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
285 order_by=order_by, order_dir=order_dir)
286 order_by=order_by, order_dir=order_dir)
286
287
287 _filtered_pull_requests = []
288 _filtered_pull_requests = []
288 for pr in pull_requests:
289 for pr in pull_requests:
289 status = pr.calculated_review_status()
290 status = pr.calculated_review_status()
290 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
291 ChangesetStatus.STATUS_UNDER_REVIEW]:
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
292 _filtered_pull_requests.append(pr)
293 _filtered_pull_requests.append(pr)
293 if length:
294 if length:
294 return _filtered_pull_requests[offset:offset+length]
295 return _filtered_pull_requests[offset:offset+length]
295 else:
296 else:
296 return _filtered_pull_requests
297 return _filtered_pull_requests
297
298
298 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
299 opened_by=None, user_id=None):
300 opened_by=None, user_id=None):
300 """
301 """
301 Count the number of pull requests for a specific repository that are
302 Count the number of pull requests for a specific repository that are
302 awaiting review from a specific user.
303 awaiting review from a specific user.
303
304
304 :param repo_name: target or source repo
305 :param repo_name: target or source repo
305 :param source: boolean flag to specify if repo_name refers to source
306 :param source: boolean flag to specify if repo_name refers to source
306 :param statuses: list of pull request statuses
307 :param statuses: list of pull request statuses
307 :param opened_by: author user of the pull request
308 :param opened_by: author user of the pull request
308 :param user_id: reviewer user of the pull request
309 :param user_id: reviewer user of the pull request
309 :returns: int number of pull requests
310 :returns: int number of pull requests
310 """
311 """
311 pull_requests = self.get_awaiting_my_review(
312 pull_requests = self.get_awaiting_my_review(
312 repo_name, source=source, statuses=statuses, opened_by=opened_by,
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
313 user_id=user_id)
314 user_id=user_id)
314
315
315 return len(pull_requests)
316 return len(pull_requests)
316
317
317 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
318 opened_by=None, user_id=None, offset=0,
319 opened_by=None, user_id=None, offset=0,
319 length=None, order_by=None, order_dir='desc'):
320 length=None, order_by=None, order_dir='desc'):
320 """
321 """
321 Get all pull requests for a specific repository that are awaiting
322 Get all pull requests for a specific repository that are awaiting
322 review from a specific user.
323 review from a specific user.
323
324
324 :param repo_name: target or source repo
325 :param repo_name: target or source repo
325 :param source: boolean flag to specify if repo_name refers to source
326 :param source: boolean flag to specify if repo_name refers to source
326 :param statuses: list of pull request statuses
327 :param statuses: list of pull request statuses
327 :param opened_by: author user of the pull request
328 :param opened_by: author user of the pull request
328 :param user_id: reviewer user of the pull request
329 :param user_id: reviewer user of the pull request
329 :param offset: pagination offset
330 :param offset: pagination offset
330 :param length: length of returned list
331 :param length: length of returned list
331 :param order_by: order of the returned list
332 :param order_by: order of the returned list
332 :param order_dir: 'asc' or 'desc' ordering direction
333 :param order_dir: 'asc' or 'desc' ordering direction
333 :returns: list of pull requests
334 :returns: list of pull requests
334 """
335 """
335 pull_requests = self.get_all(
336 pull_requests = self.get_all(
336 repo_name, source=source, statuses=statuses, opened_by=opened_by,
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
337 order_by=order_by, order_dir=order_dir)
338 order_by=order_by, order_dir=order_dir)
338
339
339 _my = PullRequestModel().get_not_reviewed(user_id)
340 _my = PullRequestModel().get_not_reviewed(user_id)
340 my_participation = []
341 my_participation = []
341 for pr in pull_requests:
342 for pr in pull_requests:
342 if pr in _my:
343 if pr in _my:
343 my_participation.append(pr)
344 my_participation.append(pr)
344 _filtered_pull_requests = my_participation
345 _filtered_pull_requests = my_participation
345 if length:
346 if length:
346 return _filtered_pull_requests[offset:offset+length]
347 return _filtered_pull_requests[offset:offset+length]
347 else:
348 else:
348 return _filtered_pull_requests
349 return _filtered_pull_requests
349
350
350 def get_not_reviewed(self, user_id):
351 def get_not_reviewed(self, user_id):
351 return [
352 return [
352 x.pull_request for x in PullRequestReviewers.query().filter(
353 x.pull_request for x in PullRequestReviewers.query().filter(
353 PullRequestReviewers.user_id == user_id).all()
354 PullRequestReviewers.user_id == user_id).all()
354 ]
355 ]
355
356
356 def _prepare_participating_query(self, user_id=None, statuses=None,
357 def _prepare_participating_query(self, user_id=None, statuses=None,
357 order_by=None, order_dir='desc'):
358 order_by=None, order_dir='desc'):
358 q = PullRequest.query()
359 q = PullRequest.query()
359 if user_id:
360 if user_id:
360 reviewers_subquery = Session().query(
361 reviewers_subquery = Session().query(
361 PullRequestReviewers.pull_request_id).filter(
362 PullRequestReviewers.pull_request_id).filter(
362 PullRequestReviewers.user_id == user_id).subquery()
363 PullRequestReviewers.user_id == user_id).subquery()
363 user_filter= or_(
364 user_filter= or_(
364 PullRequest.user_id == user_id,
365 PullRequest.user_id == user_id,
365 PullRequest.pull_request_id.in_(reviewers_subquery)
366 PullRequest.pull_request_id.in_(reviewers_subquery)
366 )
367 )
367 q = PullRequest.query().filter(user_filter)
368 q = PullRequest.query().filter(user_filter)
368
369
369 # closed,opened
370 # closed,opened
370 if statuses:
371 if statuses:
371 q = q.filter(PullRequest.status.in_(statuses))
372 q = q.filter(PullRequest.status.in_(statuses))
372
373
373 if order_by:
374 if order_by:
374 order_map = {
375 order_map = {
375 'name_raw': PullRequest.pull_request_id,
376 'name_raw': PullRequest.pull_request_id,
376 'title': PullRequest.title,
377 'title': PullRequest.title,
377 'updated_on_raw': PullRequest.updated_on,
378 'updated_on_raw': PullRequest.updated_on,
378 'target_repo': PullRequest.target_repo_id
379 'target_repo': PullRequest.target_repo_id
379 }
380 }
380 if order_dir == 'asc':
381 if order_dir == 'asc':
381 q = q.order_by(order_map[order_by].asc())
382 q = q.order_by(order_map[order_by].asc())
382 else:
383 else:
383 q = q.order_by(order_map[order_by].desc())
384 q = q.order_by(order_map[order_by].desc())
384
385
385 return q
386 return q
386
387
387 def count_im_participating_in(self, user_id=None, statuses=None):
388 def count_im_participating_in(self, user_id=None, statuses=None):
388 q = self._prepare_participating_query(user_id, statuses=statuses)
389 q = self._prepare_participating_query(user_id, statuses=statuses)
389 return q.count()
390 return q.count()
390
391
391 def get_im_participating_in(
392 def get_im_participating_in(
392 self, user_id=None, statuses=None, offset=0,
393 self, user_id=None, statuses=None, offset=0,
393 length=None, order_by=None, order_dir='desc'):
394 length=None, order_by=None, order_dir='desc'):
394 """
395 """
395 Get all Pull requests that i'm participating in, or i have opened
396 Get all Pull requests that i'm participating in, or i have opened
396 """
397 """
397
398
398 q = self._prepare_participating_query(
399 q = self._prepare_participating_query(
399 user_id, statuses=statuses, order_by=order_by,
400 user_id, statuses=statuses, order_by=order_by,
400 order_dir=order_dir)
401 order_dir=order_dir)
401
402
402 if length:
403 if length:
403 pull_requests = q.limit(length).offset(offset).all()
404 pull_requests = q.limit(length).offset(offset).all()
404 else:
405 else:
405 pull_requests = q.all()
406 pull_requests = q.all()
406
407
407 return pull_requests
408 return pull_requests
408
409
409 def get_versions(self, pull_request):
410 def get_versions(self, pull_request):
410 """
411 """
411 returns version of pull request sorted by ID descending
412 returns version of pull request sorted by ID descending
412 """
413 """
413 return PullRequestVersion.query()\
414 return PullRequestVersion.query()\
414 .filter(PullRequestVersion.pull_request == pull_request)\
415 .filter(PullRequestVersion.pull_request == pull_request)\
415 .order_by(PullRequestVersion.pull_request_version_id.asc())\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
416 .all()
417 .all()
417
418
418 def create(self, created_by, source_repo, source_ref, target_repo,
419 def create(self, created_by, source_repo, source_ref, target_repo,
419 target_ref, revisions, reviewers, title, description=None,
420 target_ref, revisions, reviewers, title, description=None,
420 reviewer_data=None):
421 reviewer_data=None):
421
422
422 created_by_user = self._get_user(created_by)
423 created_by_user = self._get_user(created_by)
423 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
424 target_repo = self._get_repo(target_repo)
425 target_repo = self._get_repo(target_repo)
425
426
426 pull_request = PullRequest()
427 pull_request = PullRequest()
427 pull_request.source_repo = source_repo
428 pull_request.source_repo = source_repo
428 pull_request.source_ref = source_ref
429 pull_request.source_ref = source_ref
429 pull_request.target_repo = target_repo
430 pull_request.target_repo = target_repo
430 pull_request.target_ref = target_ref
431 pull_request.target_ref = target_ref
431 pull_request.revisions = revisions
432 pull_request.revisions = revisions
432 pull_request.title = title
433 pull_request.title = title
433 pull_request.description = description
434 pull_request.description = description
434 pull_request.author = created_by_user
435 pull_request.author = created_by_user
435 pull_request.reviewer_data = reviewer_data
436 pull_request.reviewer_data = reviewer_data
436
437
437 Session().add(pull_request)
438 Session().add(pull_request)
438 Session().flush()
439 Session().flush()
439
440
440 reviewer_ids = set()
441 reviewer_ids = set()
441 # members / reviewers
442 # members / reviewers
442 for reviewer_object in reviewers:
443 for reviewer_object in reviewers:
443 user_id, reasons, mandatory = reviewer_object
444 user_id, reasons, mandatory = reviewer_object
444
445
445 user = self._get_user(user_id)
446 user = self._get_user(user_id)
446 reviewer_ids.add(user.user_id)
447 reviewer_ids.add(user.user_id)
447
448
448 reviewer = PullRequestReviewers()
449 reviewer = PullRequestReviewers()
449 reviewer.user = user
450 reviewer.user = user
450 reviewer.pull_request = pull_request
451 reviewer.pull_request = pull_request
451 reviewer.reasons = reasons
452 reviewer.reasons = reasons
452 reviewer.mandatory = mandatory
453 reviewer.mandatory = mandatory
453 Session().add(reviewer)
454 Session().add(reviewer)
454
455
455 # Set approval status to "Under Review" for all commits which are
456 # Set approval status to "Under Review" for all commits which are
456 # part of this pull request.
457 # part of this pull request.
457 ChangesetStatusModel().set_status(
458 ChangesetStatusModel().set_status(
458 repo=target_repo,
459 repo=target_repo,
459 status=ChangesetStatus.STATUS_UNDER_REVIEW,
460 status=ChangesetStatus.STATUS_UNDER_REVIEW,
460 user=created_by_user,
461 user=created_by_user,
461 pull_request=pull_request
462 pull_request=pull_request
462 )
463 )
463
464
464 self.notify_reviewers(pull_request, reviewer_ids)
465 self.notify_reviewers(pull_request, reviewer_ids)
465 self._trigger_pull_request_hook(
466 self._trigger_pull_request_hook(
466 pull_request, created_by_user, 'create')
467 pull_request, created_by_user, 'create')
467
468
468 return pull_request
469 return pull_request
469
470
470 def _trigger_pull_request_hook(self, pull_request, user, action):
471 def _trigger_pull_request_hook(self, pull_request, user, action):
471 pull_request = self.__get_pull_request(pull_request)
472 pull_request = self.__get_pull_request(pull_request)
472 target_scm = pull_request.target_repo.scm_instance()
473 target_scm = pull_request.target_repo.scm_instance()
473 if action == 'create':
474 if action == 'create':
474 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
475 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
475 elif action == 'merge':
476 elif action == 'merge':
476 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
477 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
477 elif action == 'close':
478 elif action == 'close':
478 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
479 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
479 elif action == 'review_status_change':
480 elif action == 'review_status_change':
480 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
481 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
481 elif action == 'update':
482 elif action == 'update':
482 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
483 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
483 else:
484 else:
484 return
485 return
485
486
486 trigger_hook(
487 trigger_hook(
487 username=user.username,
488 username=user.username,
488 repo_name=pull_request.target_repo.repo_name,
489 repo_name=pull_request.target_repo.repo_name,
489 repo_alias=target_scm.alias,
490 repo_alias=target_scm.alias,
490 pull_request=pull_request)
491 pull_request=pull_request)
491
492
492 def _get_commit_ids(self, pull_request):
493 def _get_commit_ids(self, pull_request):
493 """
494 """
494 Return the commit ids of the merged pull request.
495 Return the commit ids of the merged pull request.
495
496
496 This method is not dealing correctly yet with the lack of autoupdates
497 This method is not dealing correctly yet with the lack of autoupdates
497 nor with the implicit target updates.
498 nor with the implicit target updates.
498 For example: if a commit in the source repo is already in the target it
499 For example: if a commit in the source repo is already in the target it
499 will be reported anyways.
500 will be reported anyways.
500 """
501 """
501 merge_rev = pull_request.merge_rev
502 merge_rev = pull_request.merge_rev
502 if merge_rev is None:
503 if merge_rev is None:
503 raise ValueError('This pull request was not merged yet')
504 raise ValueError('This pull request was not merged yet')
504
505
505 commit_ids = list(pull_request.revisions)
506 commit_ids = list(pull_request.revisions)
506 if merge_rev not in commit_ids:
507 if merge_rev not in commit_ids:
507 commit_ids.append(merge_rev)
508 commit_ids.append(merge_rev)
508
509
509 return commit_ids
510 return commit_ids
510
511
511 def merge(self, pull_request, user, extras):
512 def merge(self, pull_request, user, extras):
512 log.debug("Merging pull request %s", pull_request.pull_request_id)
513 log.debug("Merging pull request %s", pull_request.pull_request_id)
513 merge_state = self._merge_pull_request(pull_request, user, extras)
514 merge_state = self._merge_pull_request(pull_request, user, extras)
514 if merge_state.executed:
515 if merge_state.executed:
515 log.debug(
516 log.debug(
516 "Merge was successful, updating the pull request comments.")
517 "Merge was successful, updating the pull request comments.")
517 self._comment_and_close_pr(pull_request, user, merge_state)
518 self._comment_and_close_pr(pull_request, user, merge_state)
518 self._log_action('user_merged_pull_request', user, pull_request)
519 self._log_action('user_merged_pull_request', user, pull_request)
519 else:
520 else:
520 log.warn("Merge failed, not updating the pull request.")
521 log.warn("Merge failed, not updating the pull request.")
521 return merge_state
522 return merge_state
522
523
523 def _merge_pull_request(self, pull_request, user, extras):
524 def _merge_pull_request(self, pull_request, user, extras):
524 target_vcs = pull_request.target_repo.scm_instance()
525 target_vcs = pull_request.target_repo.scm_instance()
525 source_vcs = pull_request.source_repo.scm_instance()
526 source_vcs = pull_request.source_repo.scm_instance()
526 target_ref = self._refresh_reference(
527 target_ref = self._refresh_reference(
527 pull_request.target_ref_parts, target_vcs)
528 pull_request.target_ref_parts, target_vcs)
528
529
529 message = _(
530 message = _(
530 'Merge pull request #%(pr_id)s from '
531 'Merge pull request #%(pr_id)s from '
531 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
532 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
532 'pr_id': pull_request.pull_request_id,
533 'pr_id': pull_request.pull_request_id,
533 'source_repo': source_vcs.name,
534 'source_repo': source_vcs.name,
534 'source_ref_name': pull_request.source_ref_parts.name,
535 'source_ref_name': pull_request.source_ref_parts.name,
535 'pr_title': pull_request.title
536 'pr_title': pull_request.title
536 }
537 }
537
538
538 workspace_id = self._workspace_id(pull_request)
539 workspace_id = self._workspace_id(pull_request)
539 use_rebase = self._use_rebase_for_merging(pull_request)
540 use_rebase = self._use_rebase_for_merging(pull_request)
540
541
541 callback_daemon, extras = prepare_callback_daemon(
542 callback_daemon, extras = prepare_callback_daemon(
542 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
543 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
543 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
544 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
544
545
545 with callback_daemon:
546 with callback_daemon:
546 # TODO: johbo: Implement a clean way to run a config_override
547 # TODO: johbo: Implement a clean way to run a config_override
547 # for a single call.
548 # for a single call.
548 target_vcs.config.set(
549 target_vcs.config.set(
549 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
550 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
550 merge_state = target_vcs.merge(
551 merge_state = target_vcs.merge(
551 target_ref, source_vcs, pull_request.source_ref_parts,
552 target_ref, source_vcs, pull_request.source_ref_parts,
552 workspace_id, user_name=user.username,
553 workspace_id, user_name=user.username,
553 user_email=user.email, message=message, use_rebase=use_rebase)
554 user_email=user.email, message=message, use_rebase=use_rebase)
554 return merge_state
555 return merge_state
555
556
556 def _comment_and_close_pr(self, pull_request, user, merge_state):
557 def _comment_and_close_pr(self, pull_request, user, merge_state):
557 pull_request.merge_rev = merge_state.merge_ref.commit_id
558 pull_request.merge_rev = merge_state.merge_ref.commit_id
558 pull_request.updated_on = datetime.datetime.now()
559 pull_request.updated_on = datetime.datetime.now()
559
560
560 CommentsModel().create(
561 CommentsModel().create(
561 text=unicode(_('Pull request merged and closed')),
562 text=unicode(_('Pull request merged and closed')),
562 repo=pull_request.target_repo.repo_id,
563 repo=pull_request.target_repo.repo_id,
563 user=user.user_id,
564 user=user.user_id,
564 pull_request=pull_request.pull_request_id,
565 pull_request=pull_request.pull_request_id,
565 f_path=None,
566 f_path=None,
566 line_no=None,
567 line_no=None,
567 closing_pr=True
568 closing_pr=True
568 )
569 )
569
570
570 Session().add(pull_request)
571 Session().add(pull_request)
571 Session().flush()
572 Session().flush()
572 # TODO: paris: replace invalidation with less radical solution
573 # TODO: paris: replace invalidation with less radical solution
573 ScmModel().mark_for_invalidation(
574 ScmModel().mark_for_invalidation(
574 pull_request.target_repo.repo_name)
575 pull_request.target_repo.repo_name)
575 self._trigger_pull_request_hook(pull_request, user, 'merge')
576 self._trigger_pull_request_hook(pull_request, user, 'merge')
576
577
577 def has_valid_update_type(self, pull_request):
578 def has_valid_update_type(self, pull_request):
578 source_ref_type = pull_request.source_ref_parts.type
579 source_ref_type = pull_request.source_ref_parts.type
579 return source_ref_type in ['book', 'branch', 'tag']
580 return source_ref_type in ['book', 'branch', 'tag']
580
581
581 def update_commits(self, pull_request):
582 def update_commits(self, pull_request):
582 """
583 """
583 Get the updated list of commits for the pull request
584 Get the updated list of commits for the pull request
584 and return the new pull request version and the list
585 and return the new pull request version and the list
585 of commits processed by this update action
586 of commits processed by this update action
586 """
587 """
587 pull_request = self.__get_pull_request(pull_request)
588 pull_request = self.__get_pull_request(pull_request)
588 source_ref_type = pull_request.source_ref_parts.type
589 source_ref_type = pull_request.source_ref_parts.type
589 source_ref_name = pull_request.source_ref_parts.name
590 source_ref_name = pull_request.source_ref_parts.name
590 source_ref_id = pull_request.source_ref_parts.commit_id
591 source_ref_id = pull_request.source_ref_parts.commit_id
591
592
592 target_ref_type = pull_request.target_ref_parts.type
593 target_ref_type = pull_request.target_ref_parts.type
593 target_ref_name = pull_request.target_ref_parts.name
594 target_ref_name = pull_request.target_ref_parts.name
594 target_ref_id = pull_request.target_ref_parts.commit_id
595 target_ref_id = pull_request.target_ref_parts.commit_id
595
596
596 if not self.has_valid_update_type(pull_request):
597 if not self.has_valid_update_type(pull_request):
597 log.debug(
598 log.debug(
598 "Skipping update of pull request %s due to ref type: %s",
599 "Skipping update of pull request %s due to ref type: %s",
599 pull_request, source_ref_type)
600 pull_request, source_ref_type)
600 return UpdateResponse(
601 return UpdateResponse(
601 executed=False,
602 executed=False,
602 reason=UpdateFailureReason.WRONG_REF_TYPE,
603 reason=UpdateFailureReason.WRONG_REF_TYPE,
603 old=pull_request, new=None, changes=None,
604 old=pull_request, new=None, changes=None,
604 source_changed=False, target_changed=False)
605 source_changed=False, target_changed=False)
605
606
606 # source repo
607 # source repo
607 source_repo = pull_request.source_repo.scm_instance()
608 source_repo = pull_request.source_repo.scm_instance()
608 try:
609 try:
609 source_commit = source_repo.get_commit(commit_id=source_ref_name)
610 source_commit = source_repo.get_commit(commit_id=source_ref_name)
610 except CommitDoesNotExistError:
611 except CommitDoesNotExistError:
611 return UpdateResponse(
612 return UpdateResponse(
612 executed=False,
613 executed=False,
613 reason=UpdateFailureReason.MISSING_SOURCE_REF,
614 reason=UpdateFailureReason.MISSING_SOURCE_REF,
614 old=pull_request, new=None, changes=None,
615 old=pull_request, new=None, changes=None,
615 source_changed=False, target_changed=False)
616 source_changed=False, target_changed=False)
616
617
617 source_changed = source_ref_id != source_commit.raw_id
618 source_changed = source_ref_id != source_commit.raw_id
618
619
619 # target repo
620 # target repo
620 target_repo = pull_request.target_repo.scm_instance()
621 target_repo = pull_request.target_repo.scm_instance()
621 try:
622 try:
622 target_commit = target_repo.get_commit(commit_id=target_ref_name)
623 target_commit = target_repo.get_commit(commit_id=target_ref_name)
623 except CommitDoesNotExistError:
624 except CommitDoesNotExistError:
624 return UpdateResponse(
625 return UpdateResponse(
625 executed=False,
626 executed=False,
626 reason=UpdateFailureReason.MISSING_TARGET_REF,
627 reason=UpdateFailureReason.MISSING_TARGET_REF,
627 old=pull_request, new=None, changes=None,
628 old=pull_request, new=None, changes=None,
628 source_changed=False, target_changed=False)
629 source_changed=False, target_changed=False)
629 target_changed = target_ref_id != target_commit.raw_id
630 target_changed = target_ref_id != target_commit.raw_id
630
631
631 if not (source_changed or target_changed):
632 if not (source_changed or target_changed):
632 log.debug("Nothing changed in pull request %s", pull_request)
633 log.debug("Nothing changed in pull request %s", pull_request)
633 return UpdateResponse(
634 return UpdateResponse(
634 executed=False,
635 executed=False,
635 reason=UpdateFailureReason.NO_CHANGE,
636 reason=UpdateFailureReason.NO_CHANGE,
636 old=pull_request, new=None, changes=None,
637 old=pull_request, new=None, changes=None,
637 source_changed=target_changed, target_changed=source_changed)
638 source_changed=target_changed, target_changed=source_changed)
638
639
639 change_in_found = 'target repo' if target_changed else 'source repo'
640 change_in_found = 'target repo' if target_changed else 'source repo'
640 log.debug('Updating pull request because of change in %s detected',
641 log.debug('Updating pull request because of change in %s detected',
641 change_in_found)
642 change_in_found)
642
643
643 # Finally there is a need for an update, in case of source change
644 # Finally there is a need for an update, in case of source change
644 # we create a new version, else just an update
645 # we create a new version, else just an update
645 if source_changed:
646 if source_changed:
646 pull_request_version = self._create_version_from_snapshot(pull_request)
647 pull_request_version = self._create_version_from_snapshot(pull_request)
647 self._link_comments_to_version(pull_request_version)
648 self._link_comments_to_version(pull_request_version)
648 else:
649 else:
649 try:
650 try:
650 ver = pull_request.versions[-1]
651 ver = pull_request.versions[-1]
651 except IndexError:
652 except IndexError:
652 ver = None
653 ver = None
653
654
654 pull_request.pull_request_version_id = \
655 pull_request.pull_request_version_id = \
655 ver.pull_request_version_id if ver else None
656 ver.pull_request_version_id if ver else None
656 pull_request_version = pull_request
657 pull_request_version = pull_request
657
658
658 try:
659 try:
659 if target_ref_type in ('tag', 'branch', 'book'):
660 if target_ref_type in ('tag', 'branch', 'book'):
660 target_commit = target_repo.get_commit(target_ref_name)
661 target_commit = target_repo.get_commit(target_ref_name)
661 else:
662 else:
662 target_commit = target_repo.get_commit(target_ref_id)
663 target_commit = target_repo.get_commit(target_ref_id)
663 except CommitDoesNotExistError:
664 except CommitDoesNotExistError:
664 return UpdateResponse(
665 return UpdateResponse(
665 executed=False,
666 executed=False,
666 reason=UpdateFailureReason.MISSING_TARGET_REF,
667 reason=UpdateFailureReason.MISSING_TARGET_REF,
667 old=pull_request, new=None, changes=None,
668 old=pull_request, new=None, changes=None,
668 source_changed=source_changed, target_changed=target_changed)
669 source_changed=source_changed, target_changed=target_changed)
669
670
670 # re-compute commit ids
671 # re-compute commit ids
671 old_commit_ids = pull_request.revisions
672 old_commit_ids = pull_request.revisions
672 pre_load = ["author", "branch", "date", "message"]
673 pre_load = ["author", "branch", "date", "message"]
673 commit_ranges = target_repo.compare(
674 commit_ranges = target_repo.compare(
674 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
675 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
675 pre_load=pre_load)
676 pre_load=pre_load)
676
677
677 ancestor = target_repo.get_common_ancestor(
678 ancestor = target_repo.get_common_ancestor(
678 target_commit.raw_id, source_commit.raw_id, source_repo)
679 target_commit.raw_id, source_commit.raw_id, source_repo)
679
680
680 pull_request.source_ref = '%s:%s:%s' % (
681 pull_request.source_ref = '%s:%s:%s' % (
681 source_ref_type, source_ref_name, source_commit.raw_id)
682 source_ref_type, source_ref_name, source_commit.raw_id)
682 pull_request.target_ref = '%s:%s:%s' % (
683 pull_request.target_ref = '%s:%s:%s' % (
683 target_ref_type, target_ref_name, ancestor)
684 target_ref_type, target_ref_name, ancestor)
684
685
685 pull_request.revisions = [
686 pull_request.revisions = [
686 commit.raw_id for commit in reversed(commit_ranges)]
687 commit.raw_id for commit in reversed(commit_ranges)]
687 pull_request.updated_on = datetime.datetime.now()
688 pull_request.updated_on = datetime.datetime.now()
688 Session().add(pull_request)
689 Session().add(pull_request)
689 new_commit_ids = pull_request.revisions
690 new_commit_ids = pull_request.revisions
690
691
691 old_diff_data, new_diff_data = self._generate_update_diffs(
692 old_diff_data, new_diff_data = self._generate_update_diffs(
692 pull_request, pull_request_version)
693 pull_request, pull_request_version)
693
694
694 # calculate commit and file changes
695 # calculate commit and file changes
695 changes = self._calculate_commit_id_changes(
696 changes = self._calculate_commit_id_changes(
696 old_commit_ids, new_commit_ids)
697 old_commit_ids, new_commit_ids)
697 file_changes = self._calculate_file_changes(
698 file_changes = self._calculate_file_changes(
698 old_diff_data, new_diff_data)
699 old_diff_data, new_diff_data)
699
700
700 # set comments as outdated if DIFFS changed
701 # set comments as outdated if DIFFS changed
701 CommentsModel().outdate_comments(
702 CommentsModel().outdate_comments(
702 pull_request, old_diff_data=old_diff_data,
703 pull_request, old_diff_data=old_diff_data,
703 new_diff_data=new_diff_data)
704 new_diff_data=new_diff_data)
704
705
705 commit_changes = (changes.added or changes.removed)
706 commit_changes = (changes.added or changes.removed)
706 file_node_changes = (
707 file_node_changes = (
707 file_changes.added or file_changes.modified or file_changes.removed)
708 file_changes.added or file_changes.modified or file_changes.removed)
708 pr_has_changes = commit_changes or file_node_changes
709 pr_has_changes = commit_changes or file_node_changes
709
710
710 # Add an automatic comment to the pull request, in case
711 # Add an automatic comment to the pull request, in case
711 # anything has changed
712 # anything has changed
712 if pr_has_changes:
713 if pr_has_changes:
713 update_comment = CommentsModel().create(
714 update_comment = CommentsModel().create(
714 text=self._render_update_message(changes, file_changes),
715 text=self._render_update_message(changes, file_changes),
715 repo=pull_request.target_repo,
716 repo=pull_request.target_repo,
716 user=pull_request.author,
717 user=pull_request.author,
717 pull_request=pull_request,
718 pull_request=pull_request,
718 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
719 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
719
720
720 # Update status to "Under Review" for added commits
721 # Update status to "Under Review" for added commits
721 for commit_id in changes.added:
722 for commit_id in changes.added:
722 ChangesetStatusModel().set_status(
723 ChangesetStatusModel().set_status(
723 repo=pull_request.source_repo,
724 repo=pull_request.source_repo,
724 status=ChangesetStatus.STATUS_UNDER_REVIEW,
725 status=ChangesetStatus.STATUS_UNDER_REVIEW,
725 comment=update_comment,
726 comment=update_comment,
726 user=pull_request.author,
727 user=pull_request.author,
727 pull_request=pull_request,
728 pull_request=pull_request,
728 revision=commit_id)
729 revision=commit_id)
729
730
730 log.debug(
731 log.debug(
731 'Updated pull request %s, added_ids: %s, common_ids: %s, '
732 'Updated pull request %s, added_ids: %s, common_ids: %s, '
732 'removed_ids: %s', pull_request.pull_request_id,
733 'removed_ids: %s', pull_request.pull_request_id,
733 changes.added, changes.common, changes.removed)
734 changes.added, changes.common, changes.removed)
734 log.debug(
735 log.debug(
735 'Updated pull request with the following file changes: %s',
736 'Updated pull request with the following file changes: %s',
736 file_changes)
737 file_changes)
737
738
738 log.info(
739 log.info(
739 "Updated pull request %s from commit %s to commit %s, "
740 "Updated pull request %s from commit %s to commit %s, "
740 "stored new version %s of this pull request.",
741 "stored new version %s of this pull request.",
741 pull_request.pull_request_id, source_ref_id,
742 pull_request.pull_request_id, source_ref_id,
742 pull_request.source_ref_parts.commit_id,
743 pull_request.source_ref_parts.commit_id,
743 pull_request_version.pull_request_version_id)
744 pull_request_version.pull_request_version_id)
744 Session().commit()
745 Session().commit()
745 self._trigger_pull_request_hook(
746 self._trigger_pull_request_hook(
746 pull_request, pull_request.author, 'update')
747 pull_request, pull_request.author, 'update')
747
748
748 return UpdateResponse(
749 return UpdateResponse(
749 executed=True, reason=UpdateFailureReason.NONE,
750 executed=True, reason=UpdateFailureReason.NONE,
750 old=pull_request, new=pull_request_version, changes=changes,
751 old=pull_request, new=pull_request_version, changes=changes,
751 source_changed=source_changed, target_changed=target_changed)
752 source_changed=source_changed, target_changed=target_changed)
752
753
753 def _create_version_from_snapshot(self, pull_request):
754 def _create_version_from_snapshot(self, pull_request):
754 version = PullRequestVersion()
755 version = PullRequestVersion()
755 version.title = pull_request.title
756 version.title = pull_request.title
756 version.description = pull_request.description
757 version.description = pull_request.description
757 version.status = pull_request.status
758 version.status = pull_request.status
758 version.created_on = datetime.datetime.now()
759 version.created_on = datetime.datetime.now()
759 version.updated_on = pull_request.updated_on
760 version.updated_on = pull_request.updated_on
760 version.user_id = pull_request.user_id
761 version.user_id = pull_request.user_id
761 version.source_repo = pull_request.source_repo
762 version.source_repo = pull_request.source_repo
762 version.source_ref = pull_request.source_ref
763 version.source_ref = pull_request.source_ref
763 version.target_repo = pull_request.target_repo
764 version.target_repo = pull_request.target_repo
764 version.target_ref = pull_request.target_ref
765 version.target_ref = pull_request.target_ref
765
766
766 version._last_merge_source_rev = pull_request._last_merge_source_rev
767 version._last_merge_source_rev = pull_request._last_merge_source_rev
767 version._last_merge_target_rev = pull_request._last_merge_target_rev
768 version._last_merge_target_rev = pull_request._last_merge_target_rev
768 version._last_merge_status = pull_request._last_merge_status
769 version._last_merge_status = pull_request._last_merge_status
769 version.shadow_merge_ref = pull_request.shadow_merge_ref
770 version.shadow_merge_ref = pull_request.shadow_merge_ref
770 version.merge_rev = pull_request.merge_rev
771 version.merge_rev = pull_request.merge_rev
771 version.reviewer_data = pull_request.reviewer_data
772 version.reviewer_data = pull_request.reviewer_data
772
773
773 version.revisions = pull_request.revisions
774 version.revisions = pull_request.revisions
774 version.pull_request = pull_request
775 version.pull_request = pull_request
775 Session().add(version)
776 Session().add(version)
776 Session().flush()
777 Session().flush()
777
778
778 return version
779 return version
779
780
780 def _generate_update_diffs(self, pull_request, pull_request_version):
781 def _generate_update_diffs(self, pull_request, pull_request_version):
781
782
782 diff_context = (
783 diff_context = (
783 self.DIFF_CONTEXT +
784 self.DIFF_CONTEXT +
784 CommentsModel.needed_extra_diff_context())
785 CommentsModel.needed_extra_diff_context())
785
786
786 source_repo = pull_request_version.source_repo
787 source_repo = pull_request_version.source_repo
787 source_ref_id = pull_request_version.source_ref_parts.commit_id
788 source_ref_id = pull_request_version.source_ref_parts.commit_id
788 target_ref_id = pull_request_version.target_ref_parts.commit_id
789 target_ref_id = pull_request_version.target_ref_parts.commit_id
789 old_diff = self._get_diff_from_pr_or_version(
790 old_diff = self._get_diff_from_pr_or_version(
790 source_repo, source_ref_id, target_ref_id, context=diff_context)
791 source_repo, source_ref_id, target_ref_id, context=diff_context)
791
792
792 source_repo = pull_request.source_repo
793 source_repo = pull_request.source_repo
793 source_ref_id = pull_request.source_ref_parts.commit_id
794 source_ref_id = pull_request.source_ref_parts.commit_id
794 target_ref_id = pull_request.target_ref_parts.commit_id
795 target_ref_id = pull_request.target_ref_parts.commit_id
795
796
796 new_diff = self._get_diff_from_pr_or_version(
797 new_diff = self._get_diff_from_pr_or_version(
797 source_repo, source_ref_id, target_ref_id, context=diff_context)
798 source_repo, source_ref_id, target_ref_id, context=diff_context)
798
799
799 old_diff_data = diffs.DiffProcessor(old_diff)
800 old_diff_data = diffs.DiffProcessor(old_diff)
800 old_diff_data.prepare()
801 old_diff_data.prepare()
801 new_diff_data = diffs.DiffProcessor(new_diff)
802 new_diff_data = diffs.DiffProcessor(new_diff)
802 new_diff_data.prepare()
803 new_diff_data.prepare()
803
804
804 return old_diff_data, new_diff_data
805 return old_diff_data, new_diff_data
805
806
806 def _link_comments_to_version(self, pull_request_version):
807 def _link_comments_to_version(self, pull_request_version):
807 """
808 """
808 Link all unlinked comments of this pull request to the given version.
809 Link all unlinked comments of this pull request to the given version.
809
810
810 :param pull_request_version: The `PullRequestVersion` to which
811 :param pull_request_version: The `PullRequestVersion` to which
811 the comments shall be linked.
812 the comments shall be linked.
812
813
813 """
814 """
814 pull_request = pull_request_version.pull_request
815 pull_request = pull_request_version.pull_request
815 comments = ChangesetComment.query()\
816 comments = ChangesetComment.query()\
816 .filter(
817 .filter(
817 # TODO: johbo: Should we query for the repo at all here?
818 # TODO: johbo: Should we query for the repo at all here?
818 # Pending decision on how comments of PRs are to be related
819 # Pending decision on how comments of PRs are to be related
819 # to either the source repo, the target repo or no repo at all.
820 # to either the source repo, the target repo or no repo at all.
820 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
821 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
821 ChangesetComment.pull_request == pull_request,
822 ChangesetComment.pull_request == pull_request,
822 ChangesetComment.pull_request_version == None)\
823 ChangesetComment.pull_request_version == None)\
823 .order_by(ChangesetComment.comment_id.asc())
824 .order_by(ChangesetComment.comment_id.asc())
824
825
825 # TODO: johbo: Find out why this breaks if it is done in a bulk
826 # TODO: johbo: Find out why this breaks if it is done in a bulk
826 # operation.
827 # operation.
827 for comment in comments:
828 for comment in comments:
828 comment.pull_request_version_id = (
829 comment.pull_request_version_id = (
829 pull_request_version.pull_request_version_id)
830 pull_request_version.pull_request_version_id)
830 Session().add(comment)
831 Session().add(comment)
831
832
832 def _calculate_commit_id_changes(self, old_ids, new_ids):
833 def _calculate_commit_id_changes(self, old_ids, new_ids):
833 added = [x for x in new_ids if x not in old_ids]
834 added = [x for x in new_ids if x not in old_ids]
834 common = [x for x in new_ids if x in old_ids]
835 common = [x for x in new_ids if x in old_ids]
835 removed = [x for x in old_ids if x not in new_ids]
836 removed = [x for x in old_ids if x not in new_ids]
836 total = new_ids
837 total = new_ids
837 return ChangeTuple(added, common, removed, total)
838 return ChangeTuple(added, common, removed, total)
838
839
839 def _calculate_file_changes(self, old_diff_data, new_diff_data):
840 def _calculate_file_changes(self, old_diff_data, new_diff_data):
840
841
841 old_files = OrderedDict()
842 old_files = OrderedDict()
842 for diff_data in old_diff_data.parsed_diff:
843 for diff_data in old_diff_data.parsed_diff:
843 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
844 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
844
845
845 added_files = []
846 added_files = []
846 modified_files = []
847 modified_files = []
847 removed_files = []
848 removed_files = []
848 for diff_data in new_diff_data.parsed_diff:
849 for diff_data in new_diff_data.parsed_diff:
849 new_filename = diff_data['filename']
850 new_filename = diff_data['filename']
850 new_hash = md5_safe(diff_data['raw_diff'])
851 new_hash = md5_safe(diff_data['raw_diff'])
851
852
852 old_hash = old_files.get(new_filename)
853 old_hash = old_files.get(new_filename)
853 if not old_hash:
854 if not old_hash:
854 # file is not present in old diff, means it's added
855 # file is not present in old diff, means it's added
855 added_files.append(new_filename)
856 added_files.append(new_filename)
856 else:
857 else:
857 if new_hash != old_hash:
858 if new_hash != old_hash:
858 modified_files.append(new_filename)
859 modified_files.append(new_filename)
859 # now remove a file from old, since we have seen it already
860 # now remove a file from old, since we have seen it already
860 del old_files[new_filename]
861 del old_files[new_filename]
861
862
862 # removed files is when there are present in old, but not in NEW,
863 # removed files is when there are present in old, but not in NEW,
863 # since we remove old files that are present in new diff, left-overs
864 # since we remove old files that are present in new diff, left-overs
864 # if any should be the removed files
865 # if any should be the removed files
865 removed_files.extend(old_files.keys())
866 removed_files.extend(old_files.keys())
866
867
867 return FileChangeTuple(added_files, modified_files, removed_files)
868 return FileChangeTuple(added_files, modified_files, removed_files)
868
869
869 def _render_update_message(self, changes, file_changes):
870 def _render_update_message(self, changes, file_changes):
870 """
871 """
871 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
872 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
872 so it's always looking the same disregarding on which default
873 so it's always looking the same disregarding on which default
873 renderer system is using.
874 renderer system is using.
874
875
875 :param changes: changes named tuple
876 :param changes: changes named tuple
876 :param file_changes: file changes named tuple
877 :param file_changes: file changes named tuple
877
878
878 """
879 """
879 new_status = ChangesetStatus.get_status_lbl(
880 new_status = ChangesetStatus.get_status_lbl(
880 ChangesetStatus.STATUS_UNDER_REVIEW)
881 ChangesetStatus.STATUS_UNDER_REVIEW)
881
882
882 changed_files = (
883 changed_files = (
883 file_changes.added + file_changes.modified + file_changes.removed)
884 file_changes.added + file_changes.modified + file_changes.removed)
884
885
885 params = {
886 params = {
886 'under_review_label': new_status,
887 'under_review_label': new_status,
887 'added_commits': changes.added,
888 'added_commits': changes.added,
888 'removed_commits': changes.removed,
889 'removed_commits': changes.removed,
889 'changed_files': changed_files,
890 'changed_files': changed_files,
890 'added_files': file_changes.added,
891 'added_files': file_changes.added,
891 'modified_files': file_changes.modified,
892 'modified_files': file_changes.modified,
892 'removed_files': file_changes.removed,
893 'removed_files': file_changes.removed,
893 }
894 }
894 renderer = RstTemplateRenderer()
895 renderer = RstTemplateRenderer()
895 return renderer.render('pull_request_update.mako', **params)
896 return renderer.render('pull_request_update.mako', **params)
896
897
897 def edit(self, pull_request, title, description):
898 def edit(self, pull_request, title, description):
898 pull_request = self.__get_pull_request(pull_request)
899 pull_request = self.__get_pull_request(pull_request)
899 if pull_request.is_closed():
900 if pull_request.is_closed():
900 raise ValueError('This pull request is closed')
901 raise ValueError('This pull request is closed')
901 if title:
902 if title:
902 pull_request.title = title
903 pull_request.title = title
903 pull_request.description = description
904 pull_request.description = description
904 pull_request.updated_on = datetime.datetime.now()
905 pull_request.updated_on = datetime.datetime.now()
905 Session().add(pull_request)
906 Session().add(pull_request)
906
907
907 def update_reviewers(self, pull_request, reviewer_data):
908 def update_reviewers(self, pull_request, reviewer_data):
908 """
909 """
909 Update the reviewers in the pull request
910 Update the reviewers in the pull request
910
911
911 :param pull_request: the pr to update
912 :param pull_request: the pr to update
912 :param reviewer_data: list of tuples
913 :param reviewer_data: list of tuples
913 [(user, ['reason1', 'reason2'], mandatory_flag)]
914 [(user, ['reason1', 'reason2'], mandatory_flag)]
914 """
915 """
915
916
916 reviewers = {}
917 reviewers = {}
917 for user_id, reasons, mandatory in reviewer_data:
918 for user_id, reasons, mandatory in reviewer_data:
918 if isinstance(user_id, (int, basestring)):
919 if isinstance(user_id, (int, basestring)):
919 user_id = self._get_user(user_id).user_id
920 user_id = self._get_user(user_id).user_id
920 reviewers[user_id] = {
921 reviewers[user_id] = {
921 'reasons': reasons, 'mandatory': mandatory}
922 'reasons': reasons, 'mandatory': mandatory}
922
923
923 reviewers_ids = set(reviewers.keys())
924 reviewers_ids = set(reviewers.keys())
924 pull_request = self.__get_pull_request(pull_request)
925 pull_request = self.__get_pull_request(pull_request)
925 current_reviewers = PullRequestReviewers.query()\
926 current_reviewers = PullRequestReviewers.query()\
926 .filter(PullRequestReviewers.pull_request ==
927 .filter(PullRequestReviewers.pull_request ==
927 pull_request).all()
928 pull_request).all()
928 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
929 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
929
930
930 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
931 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
931 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
932 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
932
933
933 log.debug("Adding %s reviewers", ids_to_add)
934 log.debug("Adding %s reviewers", ids_to_add)
934 log.debug("Removing %s reviewers", ids_to_remove)
935 log.debug("Removing %s reviewers", ids_to_remove)
935 changed = False
936 changed = False
936 for uid in ids_to_add:
937 for uid in ids_to_add:
937 changed = True
938 changed = True
938 _usr = self._get_user(uid)
939 _usr = self._get_user(uid)
939 reviewer = PullRequestReviewers()
940 reviewer = PullRequestReviewers()
940 reviewer.user = _usr
941 reviewer.user = _usr
941 reviewer.pull_request = pull_request
942 reviewer.pull_request = pull_request
942 reviewer.reasons = reviewers[uid]['reasons']
943 reviewer.reasons = reviewers[uid]['reasons']
943 # NOTE(marcink): mandatory shouldn't be changed now
944 # NOTE(marcink): mandatory shouldn't be changed now
944 #reviewer.mandatory = reviewers[uid]['reasons']
945 #reviewer.mandatory = reviewers[uid]['reasons']
945 Session().add(reviewer)
946 Session().add(reviewer)
946
947
947 for uid in ids_to_remove:
948 for uid in ids_to_remove:
948 changed = True
949 changed = True
949 reviewers = PullRequestReviewers.query()\
950 reviewers = PullRequestReviewers.query()\
950 .filter(PullRequestReviewers.user_id == uid,
951 .filter(PullRequestReviewers.user_id == uid,
951 PullRequestReviewers.pull_request == pull_request)\
952 PullRequestReviewers.pull_request == pull_request)\
952 .all()
953 .all()
953 # use .all() in case we accidentally added the same person twice
954 # use .all() in case we accidentally added the same person twice
954 # this CAN happen due to the lack of DB checks
955 # this CAN happen due to the lack of DB checks
955 for obj in reviewers:
956 for obj in reviewers:
956 Session().delete(obj)
957 Session().delete(obj)
957
958
958 if changed:
959 if changed:
959 pull_request.updated_on = datetime.datetime.now()
960 pull_request.updated_on = datetime.datetime.now()
960 Session().add(pull_request)
961 Session().add(pull_request)
961
962
962 self.notify_reviewers(pull_request, ids_to_add)
963 self.notify_reviewers(pull_request, ids_to_add)
963 return ids_to_add, ids_to_remove
964 return ids_to_add, ids_to_remove
964
965
965 def get_url(self, pull_request, request=None, permalink=False):
966 def get_url(self, pull_request, request=None, permalink=False):
966 if not request:
967 if not request:
967 request = get_current_request()
968 request = get_current_request()
968
969
969 if permalink:
970 if permalink:
970 return request.route_url(
971 return request.route_url(
971 'pull_requests_global',
972 'pull_requests_global',
972 pull_request_id=pull_request.pull_request_id,)
973 pull_request_id=pull_request.pull_request_id,)
973 else:
974 else:
974 return request.route_url(
975 return request.route_url(
975 'pullrequest_show',
976 'pullrequest_show',
976 repo_name=safe_str(pull_request.target_repo.repo_name),
977 repo_name=safe_str(pull_request.target_repo.repo_name),
977 pull_request_id=pull_request.pull_request_id,)
978 pull_request_id=pull_request.pull_request_id,)
978
979
979 def get_shadow_clone_url(self, pull_request):
980 def get_shadow_clone_url(self, pull_request):
980 """
981 """
981 Returns qualified url pointing to the shadow repository. If this pull
982 Returns qualified url pointing to the shadow repository. If this pull
982 request is closed there is no shadow repository and ``None`` will be
983 request is closed there is no shadow repository and ``None`` will be
983 returned.
984 returned.
984 """
985 """
985 if pull_request.is_closed():
986 if pull_request.is_closed():
986 return None
987 return None
987 else:
988 else:
988 pr_url = urllib.unquote(self.get_url(pull_request))
989 pr_url = urllib.unquote(self.get_url(pull_request))
989 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
990 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
990
991
991 def notify_reviewers(self, pull_request, reviewers_ids):
992 def notify_reviewers(self, pull_request, reviewers_ids):
992 # notification to reviewers
993 # notification to reviewers
993 if not reviewers_ids:
994 if not reviewers_ids:
994 return
995 return
995
996
996 pull_request_obj = pull_request
997 pull_request_obj = pull_request
997 # get the current participants of this pull request
998 # get the current participants of this pull request
998 recipients = reviewers_ids
999 recipients = reviewers_ids
999 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1000 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1000
1001
1001 pr_source_repo = pull_request_obj.source_repo
1002 pr_source_repo = pull_request_obj.source_repo
1002 pr_target_repo = pull_request_obj.target_repo
1003 pr_target_repo = pull_request_obj.target_repo
1003
1004
1004 pr_url = h.url(
1005 pr_url = h.url(
1005 'pullrequest_show',
1006 'pullrequest_show',
1006 repo_name=pr_target_repo.repo_name,
1007 repo_name=pr_target_repo.repo_name,
1007 pull_request_id=pull_request_obj.pull_request_id,
1008 pull_request_id=pull_request_obj.pull_request_id,
1008 qualified=True,)
1009 qualified=True,)
1009
1010
1010 # set some variables for email notification
1011 # set some variables for email notification
1011 pr_target_repo_url = h.route_url(
1012 pr_target_repo_url = h.route_url(
1012 'repo_summary', repo_name=pr_target_repo.repo_name)
1013 'repo_summary', repo_name=pr_target_repo.repo_name)
1013
1014
1014 pr_source_repo_url = h.route_url(
1015 pr_source_repo_url = h.route_url(
1015 'repo_summary', repo_name=pr_source_repo.repo_name)
1016 'repo_summary', repo_name=pr_source_repo.repo_name)
1016
1017
1017 # pull request specifics
1018 # pull request specifics
1018 pull_request_commits = [
1019 pull_request_commits = [
1019 (x.raw_id, x.message)
1020 (x.raw_id, x.message)
1020 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1021 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1021
1022
1022 kwargs = {
1023 kwargs = {
1023 'user': pull_request.author,
1024 'user': pull_request.author,
1024 'pull_request': pull_request_obj,
1025 'pull_request': pull_request_obj,
1025 'pull_request_commits': pull_request_commits,
1026 'pull_request_commits': pull_request_commits,
1026
1027
1027 'pull_request_target_repo': pr_target_repo,
1028 'pull_request_target_repo': pr_target_repo,
1028 'pull_request_target_repo_url': pr_target_repo_url,
1029 'pull_request_target_repo_url': pr_target_repo_url,
1029
1030
1030 'pull_request_source_repo': pr_source_repo,
1031 'pull_request_source_repo': pr_source_repo,
1031 'pull_request_source_repo_url': pr_source_repo_url,
1032 'pull_request_source_repo_url': pr_source_repo_url,
1032
1033
1033 'pull_request_url': pr_url,
1034 'pull_request_url': pr_url,
1034 }
1035 }
1035
1036
1036 # pre-generate the subject for notification itself
1037 # pre-generate the subject for notification itself
1037 (subject,
1038 (subject,
1038 _h, _e, # we don't care about those
1039 _h, _e, # we don't care about those
1039 body_plaintext) = EmailNotificationModel().render_email(
1040 body_plaintext) = EmailNotificationModel().render_email(
1040 notification_type, **kwargs)
1041 notification_type, **kwargs)
1041
1042
1042 # create notification objects, and emails
1043 # create notification objects, and emails
1043 NotificationModel().create(
1044 NotificationModel().create(
1044 created_by=pull_request.author,
1045 created_by=pull_request.author,
1045 notification_subject=subject,
1046 notification_subject=subject,
1046 notification_body=body_plaintext,
1047 notification_body=body_plaintext,
1047 notification_type=notification_type,
1048 notification_type=notification_type,
1048 recipients=recipients,
1049 recipients=recipients,
1049 email_kwargs=kwargs,
1050 email_kwargs=kwargs,
1050 )
1051 )
1051
1052
1052 def delete(self, pull_request):
1053 def delete(self, pull_request):
1053 pull_request = self.__get_pull_request(pull_request)
1054 pull_request = self.__get_pull_request(pull_request)
1054 self._cleanup_merge_workspace(pull_request)
1055 self._cleanup_merge_workspace(pull_request)
1055 Session().delete(pull_request)
1056 Session().delete(pull_request)
1056
1057
1057 def close_pull_request(self, pull_request, user):
1058 def close_pull_request(self, pull_request, user):
1058 pull_request = self.__get_pull_request(pull_request)
1059 pull_request = self.__get_pull_request(pull_request)
1059 self._cleanup_merge_workspace(pull_request)
1060 self._cleanup_merge_workspace(pull_request)
1060 pull_request.status = PullRequest.STATUS_CLOSED
1061 pull_request.status = PullRequest.STATUS_CLOSED
1061 pull_request.updated_on = datetime.datetime.now()
1062 pull_request.updated_on = datetime.datetime.now()
1062 Session().add(pull_request)
1063 Session().add(pull_request)
1063 self._trigger_pull_request_hook(
1064 self._trigger_pull_request_hook(
1064 pull_request, pull_request.author, 'close')
1065 pull_request, pull_request.author, 'close')
1065 self._log_action('user_closed_pull_request', user, pull_request)
1066 self._log_action('user_closed_pull_request', user, pull_request)
1066
1067
1067 def close_pull_request_with_comment(self, pull_request, user, repo,
1068 def close_pull_request_with_comment(
1068 message=None):
1069 self, pull_request, user, repo, message=None):
1069 status = ChangesetStatus.STATUS_REJECTED
1070
1071 pull_request_review_status = pull_request.calculated_review_status()
1070
1072
1071 if not message:
1073 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1072 message = (
1074 # approved only if we have voting consent
1073 _('Status change %(transition_icon)s %(status)s') % {
1075 status = ChangesetStatus.STATUS_APPROVED
1074 'transition_icon': '>',
1076 else:
1075 'status': ChangesetStatus.get_status_lbl(status)})
1077 status = ChangesetStatus.STATUS_REJECTED
1078 status_lbl = ChangesetStatus.get_status_lbl(status)
1076
1079
1077 internal_message = _('Closing with') + ' ' + message
1080 default_message = (
1081 _('Closing with status change {transition_icon} {status}.')
1082 ).format(transition_icon='>', status=status_lbl)
1083 text = message or default_message
1078
1084
1079 comm = CommentsModel().create(
1085 # create a comment, and link it to new status
1080 text=internal_message,
1086 comment = CommentsModel().create(
1087 text=text,
1081 repo=repo.repo_id,
1088 repo=repo.repo_id,
1082 user=user.user_id,
1089 user=user.user_id,
1083 pull_request=pull_request.pull_request_id,
1090 pull_request=pull_request.pull_request_id,
1084 f_path=None,
1091 status_change=status_lbl,
1085 line_no=None,
1086 status_change=ChangesetStatus.get_status_lbl(status),
1087 status_change_type=status,
1092 status_change_type=status,
1088 closing_pr=True
1093 closing_pr=True
1089 )
1094 )
1090
1095
1096 # calculate old status before we change it
1097 old_calculated_status = pull_request.calculated_review_status()
1091 ChangesetStatusModel().set_status(
1098 ChangesetStatusModel().set_status(
1092 repo.repo_id,
1099 repo.repo_id,
1093 status,
1100 status,
1094 user.user_id,
1101 user.user_id,
1095 comm,
1102 comment=comment,
1096 pull_request=pull_request.pull_request_id
1103 pull_request=pull_request.pull_request_id
1097 )
1104 )
1105
1098 Session().flush()
1106 Session().flush()
1107 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1108 # we now calculate the status of pull request again, and based on that
1109 # calculation trigger status change. This might happen in cases
1110 # that non-reviewer admin closes a pr, which means his vote doesn't
1111 # change the status, while if he's a reviewer this might change it.
1112 calculated_status = pull_request.calculated_review_status()
1113 if old_calculated_status != calculated_status:
1114 self._trigger_pull_request_hook(
1115 pull_request, user, 'review_status_change')
1099
1116
1117 # finally close the PR
1100 PullRequestModel().close_pull_request(
1118 PullRequestModel().close_pull_request(
1101 pull_request.pull_request_id, user)
1119 pull_request.pull_request_id, user)
1102
1120
1121 return comment, status
1122
1103 def merge_status(self, pull_request):
1123 def merge_status(self, pull_request):
1104 if not self._is_merge_enabled(pull_request):
1124 if not self._is_merge_enabled(pull_request):
1105 return False, _('Server-side pull request merging is disabled.')
1125 return False, _('Server-side pull request merging is disabled.')
1106 if pull_request.is_closed():
1126 if pull_request.is_closed():
1107 return False, _('This pull request is closed.')
1127 return False, _('This pull request is closed.')
1108 merge_possible, msg = self._check_repo_requirements(
1128 merge_possible, msg = self._check_repo_requirements(
1109 target=pull_request.target_repo, source=pull_request.source_repo)
1129 target=pull_request.target_repo, source=pull_request.source_repo)
1110 if not merge_possible:
1130 if not merge_possible:
1111 return merge_possible, msg
1131 return merge_possible, msg
1112
1132
1113 try:
1133 try:
1114 resp = self._try_merge(pull_request)
1134 resp = self._try_merge(pull_request)
1115 log.debug("Merge response: %s", resp)
1135 log.debug("Merge response: %s", resp)
1116 status = resp.possible, self.merge_status_message(
1136 status = resp.possible, self.merge_status_message(
1117 resp.failure_reason)
1137 resp.failure_reason)
1118 except NotImplementedError:
1138 except NotImplementedError:
1119 status = False, _('Pull request merging is not supported.')
1139 status = False, _('Pull request merging is not supported.')
1120
1140
1121 return status
1141 return status
1122
1142
1123 def _check_repo_requirements(self, target, source):
1143 def _check_repo_requirements(self, target, source):
1124 """
1144 """
1125 Check if `target` and `source` have compatible requirements.
1145 Check if `target` and `source` have compatible requirements.
1126
1146
1127 Currently this is just checking for largefiles.
1147 Currently this is just checking for largefiles.
1128 """
1148 """
1129 target_has_largefiles = self._has_largefiles(target)
1149 target_has_largefiles = self._has_largefiles(target)
1130 source_has_largefiles = self._has_largefiles(source)
1150 source_has_largefiles = self._has_largefiles(source)
1131 merge_possible = True
1151 merge_possible = True
1132 message = u''
1152 message = u''
1133
1153
1134 if target_has_largefiles != source_has_largefiles:
1154 if target_has_largefiles != source_has_largefiles:
1135 merge_possible = False
1155 merge_possible = False
1136 if source_has_largefiles:
1156 if source_has_largefiles:
1137 message = _(
1157 message = _(
1138 'Target repository large files support is disabled.')
1158 'Target repository large files support is disabled.')
1139 else:
1159 else:
1140 message = _(
1160 message = _(
1141 'Source repository large files support is disabled.')
1161 'Source repository large files support is disabled.')
1142
1162
1143 return merge_possible, message
1163 return merge_possible, message
1144
1164
1145 def _has_largefiles(self, repo):
1165 def _has_largefiles(self, repo):
1146 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1166 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1147 'extensions', 'largefiles')
1167 'extensions', 'largefiles')
1148 return largefiles_ui and largefiles_ui[0].active
1168 return largefiles_ui and largefiles_ui[0].active
1149
1169
1150 def _try_merge(self, pull_request):
1170 def _try_merge(self, pull_request):
1151 """
1171 """
1152 Try to merge the pull request and return the merge status.
1172 Try to merge the pull request and return the merge status.
1153 """
1173 """
1154 log.debug(
1174 log.debug(
1155 "Trying out if the pull request %s can be merged.",
1175 "Trying out if the pull request %s can be merged.",
1156 pull_request.pull_request_id)
1176 pull_request.pull_request_id)
1157 target_vcs = pull_request.target_repo.scm_instance()
1177 target_vcs = pull_request.target_repo.scm_instance()
1158
1178
1159 # Refresh the target reference.
1179 # Refresh the target reference.
1160 try:
1180 try:
1161 target_ref = self._refresh_reference(
1181 target_ref = self._refresh_reference(
1162 pull_request.target_ref_parts, target_vcs)
1182 pull_request.target_ref_parts, target_vcs)
1163 except CommitDoesNotExistError:
1183 except CommitDoesNotExistError:
1164 merge_state = MergeResponse(
1184 merge_state = MergeResponse(
1165 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1185 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1166 return merge_state
1186 return merge_state
1167
1187
1168 target_locked = pull_request.target_repo.locked
1188 target_locked = pull_request.target_repo.locked
1169 if target_locked and target_locked[0]:
1189 if target_locked and target_locked[0]:
1170 log.debug("The target repository is locked.")
1190 log.debug("The target repository is locked.")
1171 merge_state = MergeResponse(
1191 merge_state = MergeResponse(
1172 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1192 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1173 elif self._needs_merge_state_refresh(pull_request, target_ref):
1193 elif self._needs_merge_state_refresh(pull_request, target_ref):
1174 log.debug("Refreshing the merge status of the repository.")
1194 log.debug("Refreshing the merge status of the repository.")
1175 merge_state = self._refresh_merge_state(
1195 merge_state = self._refresh_merge_state(
1176 pull_request, target_vcs, target_ref)
1196 pull_request, target_vcs, target_ref)
1177 else:
1197 else:
1178 possible = pull_request.\
1198 possible = pull_request.\
1179 _last_merge_status == MergeFailureReason.NONE
1199 _last_merge_status == MergeFailureReason.NONE
1180 merge_state = MergeResponse(
1200 merge_state = MergeResponse(
1181 possible, False, None, pull_request._last_merge_status)
1201 possible, False, None, pull_request._last_merge_status)
1182
1202
1183 return merge_state
1203 return merge_state
1184
1204
1185 def _refresh_reference(self, reference, vcs_repository):
1205 def _refresh_reference(self, reference, vcs_repository):
1186 if reference.type in ('branch', 'book'):
1206 if reference.type in ('branch', 'book'):
1187 name_or_id = reference.name
1207 name_or_id = reference.name
1188 else:
1208 else:
1189 name_or_id = reference.commit_id
1209 name_or_id = reference.commit_id
1190 refreshed_commit = vcs_repository.get_commit(name_or_id)
1210 refreshed_commit = vcs_repository.get_commit(name_or_id)
1191 refreshed_reference = Reference(
1211 refreshed_reference = Reference(
1192 reference.type, reference.name, refreshed_commit.raw_id)
1212 reference.type, reference.name, refreshed_commit.raw_id)
1193 return refreshed_reference
1213 return refreshed_reference
1194
1214
1195 def _needs_merge_state_refresh(self, pull_request, target_reference):
1215 def _needs_merge_state_refresh(self, pull_request, target_reference):
1196 return not(
1216 return not(
1197 pull_request.revisions and
1217 pull_request.revisions and
1198 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1218 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1199 target_reference.commit_id == pull_request._last_merge_target_rev)
1219 target_reference.commit_id == pull_request._last_merge_target_rev)
1200
1220
1201 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1221 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1202 workspace_id = self._workspace_id(pull_request)
1222 workspace_id = self._workspace_id(pull_request)
1203 source_vcs = pull_request.source_repo.scm_instance()
1223 source_vcs = pull_request.source_repo.scm_instance()
1204 use_rebase = self._use_rebase_for_merging(pull_request)
1224 use_rebase = self._use_rebase_for_merging(pull_request)
1205 merge_state = target_vcs.merge(
1225 merge_state = target_vcs.merge(
1206 target_reference, source_vcs, pull_request.source_ref_parts,
1226 target_reference, source_vcs, pull_request.source_ref_parts,
1207 workspace_id, dry_run=True, use_rebase=use_rebase)
1227 workspace_id, dry_run=True, use_rebase=use_rebase)
1208
1228
1209 # Do not store the response if there was an unknown error.
1229 # Do not store the response if there was an unknown error.
1210 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1230 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1211 pull_request._last_merge_source_rev = \
1231 pull_request._last_merge_source_rev = \
1212 pull_request.source_ref_parts.commit_id
1232 pull_request.source_ref_parts.commit_id
1213 pull_request._last_merge_target_rev = target_reference.commit_id
1233 pull_request._last_merge_target_rev = target_reference.commit_id
1214 pull_request._last_merge_status = merge_state.failure_reason
1234 pull_request._last_merge_status = merge_state.failure_reason
1215 pull_request.shadow_merge_ref = merge_state.merge_ref
1235 pull_request.shadow_merge_ref = merge_state.merge_ref
1216 Session().add(pull_request)
1236 Session().add(pull_request)
1217 Session().commit()
1237 Session().commit()
1218
1238
1219 return merge_state
1239 return merge_state
1220
1240
1221 def _workspace_id(self, pull_request):
1241 def _workspace_id(self, pull_request):
1222 workspace_id = 'pr-%s' % pull_request.pull_request_id
1242 workspace_id = 'pr-%s' % pull_request.pull_request_id
1223 return workspace_id
1243 return workspace_id
1224
1244
1225 def merge_status_message(self, status_code):
1245 def merge_status_message(self, status_code):
1226 """
1246 """
1227 Return a human friendly error message for the given merge status code.
1247 Return a human friendly error message for the given merge status code.
1228 """
1248 """
1229 return self.MERGE_STATUS_MESSAGES[status_code]
1249 return self.MERGE_STATUS_MESSAGES[status_code]
1230
1250
1231 def generate_repo_data(self, repo, commit_id=None, branch=None,
1251 def generate_repo_data(self, repo, commit_id=None, branch=None,
1232 bookmark=None):
1252 bookmark=None):
1233 all_refs, selected_ref = \
1253 all_refs, selected_ref = \
1234 self._get_repo_pullrequest_sources(
1254 self._get_repo_pullrequest_sources(
1235 repo.scm_instance(), commit_id=commit_id,
1255 repo.scm_instance(), commit_id=commit_id,
1236 branch=branch, bookmark=bookmark)
1256 branch=branch, bookmark=bookmark)
1237
1257
1238 refs_select2 = []
1258 refs_select2 = []
1239 for element in all_refs:
1259 for element in all_refs:
1240 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1260 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1241 refs_select2.append({'text': element[1], 'children': children})
1261 refs_select2.append({'text': element[1], 'children': children})
1242
1262
1243 return {
1263 return {
1244 'user': {
1264 'user': {
1245 'user_id': repo.user.user_id,
1265 'user_id': repo.user.user_id,
1246 'username': repo.user.username,
1266 'username': repo.user.username,
1247 'firstname': repo.user.firstname,
1267 'firstname': repo.user.firstname,
1248 'lastname': repo.user.lastname,
1268 'lastname': repo.user.lastname,
1249 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1269 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1250 },
1270 },
1251 'description': h.chop_at_smart(repo.description, '\n'),
1271 'description': h.chop_at_smart(repo.description, '\n'),
1252 'refs': {
1272 'refs': {
1253 'all_refs': all_refs,
1273 'all_refs': all_refs,
1254 'selected_ref': selected_ref,
1274 'selected_ref': selected_ref,
1255 'select2_refs': refs_select2
1275 'select2_refs': refs_select2
1256 }
1276 }
1257 }
1277 }
1258
1278
1259 def generate_pullrequest_title(self, source, source_ref, target):
1279 def generate_pullrequest_title(self, source, source_ref, target):
1260 return u'{source}#{at_ref} to {target}'.format(
1280 return u'{source}#{at_ref} to {target}'.format(
1261 source=source,
1281 source=source,
1262 at_ref=source_ref,
1282 at_ref=source_ref,
1263 target=target,
1283 target=target,
1264 )
1284 )
1265
1285
1266 def _cleanup_merge_workspace(self, pull_request):
1286 def _cleanup_merge_workspace(self, pull_request):
1267 # Merging related cleanup
1287 # Merging related cleanup
1268 target_scm = pull_request.target_repo.scm_instance()
1288 target_scm = pull_request.target_repo.scm_instance()
1269 workspace_id = 'pr-%s' % pull_request.pull_request_id
1289 workspace_id = 'pr-%s' % pull_request.pull_request_id
1270
1290
1271 try:
1291 try:
1272 target_scm.cleanup_merge_workspace(workspace_id)
1292 target_scm.cleanup_merge_workspace(workspace_id)
1273 except NotImplementedError:
1293 except NotImplementedError:
1274 pass
1294 pass
1275
1295
1276 def _get_repo_pullrequest_sources(
1296 def _get_repo_pullrequest_sources(
1277 self, repo, commit_id=None, branch=None, bookmark=None):
1297 self, repo, commit_id=None, branch=None, bookmark=None):
1278 """
1298 """
1279 Return a structure with repo's interesting commits, suitable for
1299 Return a structure with repo's interesting commits, suitable for
1280 the selectors in pullrequest controller
1300 the selectors in pullrequest controller
1281
1301
1282 :param commit_id: a commit that must be in the list somehow
1302 :param commit_id: a commit that must be in the list somehow
1283 and selected by default
1303 and selected by default
1284 :param branch: a branch that must be in the list and selected
1304 :param branch: a branch that must be in the list and selected
1285 by default - even if closed
1305 by default - even if closed
1286 :param bookmark: a bookmark that must be in the list and selected
1306 :param bookmark: a bookmark that must be in the list and selected
1287 """
1307 """
1288
1308
1289 commit_id = safe_str(commit_id) if commit_id else None
1309 commit_id = safe_str(commit_id) if commit_id else None
1290 branch = safe_str(branch) if branch else None
1310 branch = safe_str(branch) if branch else None
1291 bookmark = safe_str(bookmark) if bookmark else None
1311 bookmark = safe_str(bookmark) if bookmark else None
1292
1312
1293 selected = None
1313 selected = None
1294
1314
1295 # order matters: first source that has commit_id in it will be selected
1315 # order matters: first source that has commit_id in it will be selected
1296 sources = []
1316 sources = []
1297 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1317 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1298 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1318 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1299
1319
1300 if commit_id:
1320 if commit_id:
1301 ref_commit = (h.short_id(commit_id), commit_id)
1321 ref_commit = (h.short_id(commit_id), commit_id)
1302 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1322 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1303
1323
1304 sources.append(
1324 sources.append(
1305 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1325 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1306 )
1326 )
1307
1327
1308 groups = []
1328 groups = []
1309 for group_key, ref_list, group_name, match in sources:
1329 for group_key, ref_list, group_name, match in sources:
1310 group_refs = []
1330 group_refs = []
1311 for ref_name, ref_id in ref_list:
1331 for ref_name, ref_id in ref_list:
1312 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1332 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1313 group_refs.append((ref_key, ref_name))
1333 group_refs.append((ref_key, ref_name))
1314
1334
1315 if not selected:
1335 if not selected:
1316 if set([commit_id, match]) & set([ref_id, ref_name]):
1336 if set([commit_id, match]) & set([ref_id, ref_name]):
1317 selected = ref_key
1337 selected = ref_key
1318
1338
1319 if group_refs:
1339 if group_refs:
1320 groups.append((group_refs, group_name))
1340 groups.append((group_refs, group_name))
1321
1341
1322 if not selected:
1342 if not selected:
1323 ref = commit_id or branch or bookmark
1343 ref = commit_id or branch or bookmark
1324 if ref:
1344 if ref:
1325 raise CommitDoesNotExistError(
1345 raise CommitDoesNotExistError(
1326 'No commit refs could be found matching: %s' % ref)
1346 'No commit refs could be found matching: %s' % ref)
1327 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1347 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1328 selected = 'branch:%s:%s' % (
1348 selected = 'branch:%s:%s' % (
1329 repo.DEFAULT_BRANCH_NAME,
1349 repo.DEFAULT_BRANCH_NAME,
1330 repo.branches[repo.DEFAULT_BRANCH_NAME]
1350 repo.branches[repo.DEFAULT_BRANCH_NAME]
1331 )
1351 )
1332 elif repo.commit_ids:
1352 elif repo.commit_ids:
1333 rev = repo.commit_ids[0]
1353 rev = repo.commit_ids[0]
1334 selected = 'rev:%s:%s' % (rev, rev)
1354 selected = 'rev:%s:%s' % (rev, rev)
1335 else:
1355 else:
1336 raise EmptyRepositoryError()
1356 raise EmptyRepositoryError()
1337 return groups, selected
1357 return groups, selected
1338
1358
1339 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1359 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1340 return self._get_diff_from_pr_or_version(
1360 return self._get_diff_from_pr_or_version(
1341 source_repo, source_ref_id, target_ref_id, context=context)
1361 source_repo, source_ref_id, target_ref_id, context=context)
1342
1362
1343 def _get_diff_from_pr_or_version(
1363 def _get_diff_from_pr_or_version(
1344 self, source_repo, source_ref_id, target_ref_id, context):
1364 self, source_repo, source_ref_id, target_ref_id, context):
1345 target_commit = source_repo.get_commit(
1365 target_commit = source_repo.get_commit(
1346 commit_id=safe_str(target_ref_id))
1366 commit_id=safe_str(target_ref_id))
1347 source_commit = source_repo.get_commit(
1367 source_commit = source_repo.get_commit(
1348 commit_id=safe_str(source_ref_id))
1368 commit_id=safe_str(source_ref_id))
1349 if isinstance(source_repo, Repository):
1369 if isinstance(source_repo, Repository):
1350 vcs_repo = source_repo.scm_instance()
1370 vcs_repo = source_repo.scm_instance()
1351 else:
1371 else:
1352 vcs_repo = source_repo
1372 vcs_repo = source_repo
1353
1373
1354 # TODO: johbo: In the context of an update, we cannot reach
1374 # TODO: johbo: In the context of an update, we cannot reach
1355 # the old commit anymore with our normal mechanisms. It needs
1375 # the old commit anymore with our normal mechanisms. It needs
1356 # some sort of special support in the vcs layer to avoid this
1376 # some sort of special support in the vcs layer to avoid this
1357 # workaround.
1377 # workaround.
1358 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1378 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1359 vcs_repo.alias == 'git'):
1379 vcs_repo.alias == 'git'):
1360 source_commit.raw_id = safe_str(source_ref_id)
1380 source_commit.raw_id = safe_str(source_ref_id)
1361
1381
1362 log.debug('calculating diff between '
1382 log.debug('calculating diff between '
1363 'source_ref:%s and target_ref:%s for repo `%s`',
1383 'source_ref:%s and target_ref:%s for repo `%s`',
1364 target_ref_id, source_ref_id,
1384 target_ref_id, source_ref_id,
1365 safe_unicode(vcs_repo.path))
1385 safe_unicode(vcs_repo.path))
1366
1386
1367 vcs_diff = vcs_repo.get_diff(
1387 vcs_diff = vcs_repo.get_diff(
1368 commit1=target_commit, commit2=source_commit, context=context)
1388 commit1=target_commit, commit2=source_commit, context=context)
1369 return vcs_diff
1389 return vcs_diff
1370
1390
1371 def _is_merge_enabled(self, pull_request):
1391 def _is_merge_enabled(self, pull_request):
1372 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1392 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1373 settings = settings_model.get_general_settings()
1393 settings = settings_model.get_general_settings()
1374 return settings.get('rhodecode_pr_merge_enabled', False)
1394 return settings.get('rhodecode_pr_merge_enabled', False)
1375
1395
1376 def _use_rebase_for_merging(self, pull_request):
1396 def _use_rebase_for_merging(self, pull_request):
1377 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1397 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1378 settings = settings_model.get_general_settings()
1398 settings = settings_model.get_general_settings()
1379 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1399 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1380
1400
1381 def _log_action(self, action, user, pull_request):
1401 def _log_action(self, action, user, pull_request):
1382 action_logger(
1402 action_logger(
1383 user,
1403 user,
1384 '{action}:{pr_id}'.format(
1404 '{action}:{pr_id}'.format(
1385 action=action, pr_id=pull_request.pull_request_id),
1405 action=action, pr_id=pull_request.pull_request_id),
1386 pull_request.target_repo)
1406 pull_request.target_repo)
1387
1407
1388 def get_reviewer_functions(self):
1408 def get_reviewer_functions(self):
1389 """
1409 """
1390 Fetches functions for validation and fetching default reviewers.
1410 Fetches functions for validation and fetching default reviewers.
1391 If available we use the EE package, else we fallback to CE
1411 If available we use the EE package, else we fallback to CE
1392 package functions
1412 package functions
1393 """
1413 """
1394 try:
1414 try:
1395 from rc_reviewers.utils import get_default_reviewers_data
1415 from rc_reviewers.utils import get_default_reviewers_data
1396 from rc_reviewers.utils import validate_default_reviewers
1416 from rc_reviewers.utils import validate_default_reviewers
1397 except ImportError:
1417 except ImportError:
1398 from rhodecode.apps.repository.utils import \
1418 from rhodecode.apps.repository.utils import \
1399 get_default_reviewers_data
1419 get_default_reviewers_data
1400 from rhodecode.apps.repository.utils import \
1420 from rhodecode.apps.repository.utils import \
1401 validate_default_reviewers
1421 validate_default_reviewers
1402
1422
1403 return get_default_reviewers_data, validate_default_reviewers
1423 return get_default_reviewers_data, validate_default_reviewers
1404
1424
1405
1425
1406 class MergeCheck(object):
1426 class MergeCheck(object):
1407 """
1427 """
1408 Perform Merge Checks and returns a check object which stores information
1428 Perform Merge Checks and returns a check object which stores information
1409 about merge errors, and merge conditions
1429 about merge errors, and merge conditions
1410 """
1430 """
1411 TODO_CHECK = 'todo'
1431 TODO_CHECK = 'todo'
1412 PERM_CHECK = 'perm'
1432 PERM_CHECK = 'perm'
1413 REVIEW_CHECK = 'review'
1433 REVIEW_CHECK = 'review'
1414 MERGE_CHECK = 'merge'
1434 MERGE_CHECK = 'merge'
1415
1435
1416 def __init__(self):
1436 def __init__(self):
1417 self.review_status = None
1437 self.review_status = None
1418 self.merge_possible = None
1438 self.merge_possible = None
1419 self.merge_msg = ''
1439 self.merge_msg = ''
1420 self.failed = None
1440 self.failed = None
1421 self.errors = []
1441 self.errors = []
1422 self.error_details = OrderedDict()
1442 self.error_details = OrderedDict()
1423
1443
1424 def push_error(self, error_type, message, error_key, details):
1444 def push_error(self, error_type, message, error_key, details):
1425 self.failed = True
1445 self.failed = True
1426 self.errors.append([error_type, message])
1446 self.errors.append([error_type, message])
1427 self.error_details[error_key] = dict(
1447 self.error_details[error_key] = dict(
1428 details=details,
1448 details=details,
1429 error_type=error_type,
1449 error_type=error_type,
1430 message=message
1450 message=message
1431 )
1451 )
1432
1452
1433 @classmethod
1453 @classmethod
1434 def validate(cls, pull_request, user, fail_early=False, translator=None):
1454 def validate(cls, pull_request, user, fail_early=False, translator=None):
1435 # if migrated to pyramid...
1455 # if migrated to pyramid...
1436 # _ = lambda: translator or _ # use passed in translator if any
1456 # _ = lambda: translator or _ # use passed in translator if any
1437
1457
1438 merge_check = cls()
1458 merge_check = cls()
1439
1459
1440 # permissions to merge
1460 # permissions to merge
1441 user_allowed_to_merge = PullRequestModel().check_user_merge(
1461 user_allowed_to_merge = PullRequestModel().check_user_merge(
1442 pull_request, user)
1462 pull_request, user)
1443 if not user_allowed_to_merge:
1463 if not user_allowed_to_merge:
1444 log.debug("MergeCheck: cannot merge, approval is pending.")
1464 log.debug("MergeCheck: cannot merge, approval is pending.")
1445
1465
1446 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1466 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1447 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1467 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1448 if fail_early:
1468 if fail_early:
1449 return merge_check
1469 return merge_check
1450
1470
1451 # review status, must be always present
1471 # review status, must be always present
1452 review_status = pull_request.calculated_review_status()
1472 review_status = pull_request.calculated_review_status()
1453 merge_check.review_status = review_status
1473 merge_check.review_status = review_status
1454
1474
1455 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1475 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1456 if not status_approved:
1476 if not status_approved:
1457 log.debug("MergeCheck: cannot merge, approval is pending.")
1477 log.debug("MergeCheck: cannot merge, approval is pending.")
1458
1478
1459 msg = _('Pull request reviewer approval is pending.')
1479 msg = _('Pull request reviewer approval is pending.')
1460
1480
1461 merge_check.push_error(
1481 merge_check.push_error(
1462 'warning', msg, cls.REVIEW_CHECK, review_status)
1482 'warning', msg, cls.REVIEW_CHECK, review_status)
1463
1483
1464 if fail_early:
1484 if fail_early:
1465 return merge_check
1485 return merge_check
1466
1486
1467 # left over TODOs
1487 # left over TODOs
1468 todos = CommentsModel().get_unresolved_todos(pull_request)
1488 todos = CommentsModel().get_unresolved_todos(pull_request)
1469 if todos:
1489 if todos:
1470 log.debug("MergeCheck: cannot merge, {} "
1490 log.debug("MergeCheck: cannot merge, {} "
1471 "unresolved todos left.".format(len(todos)))
1491 "unresolved todos left.".format(len(todos)))
1472
1492
1473 if len(todos) == 1:
1493 if len(todos) == 1:
1474 msg = _('Cannot merge, {} TODO still not resolved.').format(
1494 msg = _('Cannot merge, {} TODO still not resolved.').format(
1475 len(todos))
1495 len(todos))
1476 else:
1496 else:
1477 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1497 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1478 len(todos))
1498 len(todos))
1479
1499
1480 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1500 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1481
1501
1482 if fail_early:
1502 if fail_early:
1483 return merge_check
1503 return merge_check
1484
1504
1485 # merge possible
1505 # merge possible
1486 merge_status, msg = PullRequestModel().merge_status(pull_request)
1506 merge_status, msg = PullRequestModel().merge_status(pull_request)
1487 merge_check.merge_possible = merge_status
1507 merge_check.merge_possible = merge_status
1488 merge_check.merge_msg = msg
1508 merge_check.merge_msg = msg
1489 if not merge_status:
1509 if not merge_status:
1490 log.debug(
1510 log.debug(
1491 "MergeCheck: cannot merge, pull request merge not possible.")
1511 "MergeCheck: cannot merge, pull request merge not possible.")
1492 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1512 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1493
1513
1494 if fail_early:
1514 if fail_early:
1495 return merge_check
1515 return merge_check
1496
1516
1497 return merge_check
1517 return merge_check
1498
1518
1499
1519
1500 ChangeTuple = namedtuple('ChangeTuple',
1520 ChangeTuple = namedtuple('ChangeTuple',
1501 ['added', 'common', 'removed', 'total'])
1521 ['added', 'common', 'removed', 'total'])
1502
1522
1503 FileChangeTuple = namedtuple('FileChangeTuple',
1523 FileChangeTuple = namedtuple('FileChangeTuple',
1504 ['added', 'modified', 'removed'])
1524 ['added', 'modified', 'removed'])
@@ -1,138 +1,139 b''
1
1
2 /******************************************************************************
2 /******************************************************************************
3 * *
3 * *
4 * DO NOT CHANGE THIS FILE MANUALLY *
4 * DO NOT CHANGE THIS FILE MANUALLY *
5 * *
5 * *
6 * *
6 * *
7 * This file is automatically generated when the app starts up with *
7 * This file is automatically generated when the app starts up with *
8 * generate_js_files = true *
8 * generate_js_files = true *
9 * *
9 * *
10 * To add a route here pass jsroute=True to the route definition in the app *
10 * To add a route here pass jsroute=True to the route definition in the app *
11 * *
11 * *
12 ******************************************************************************/
12 ******************************************************************************/
13 function registerRCRoutes() {
13 function registerRCRoutes() {
14 // routes registration
14 // routes registration
15 pyroutes.register('new_repo', '/_admin/create_repository', []);
15 pyroutes.register('new_repo', '/_admin/create_repository', []);
16 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
16 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
17 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
17 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
18 pyroutes.register('gists', '/_admin/gists', []);
18 pyroutes.register('gists', '/_admin/gists', []);
19 pyroutes.register('new_gist', '/_admin/gists/new', []);
19 pyroutes.register('new_gist', '/_admin/gists/new', []);
20 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
20 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
21 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
21 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
22 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
22 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
23 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
23 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
24 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
24 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
25 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
25 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
26 pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
26 pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
27 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
27 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
28 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
28 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
29 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
29 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
30 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
30 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
31 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
31 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
32 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
32 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
33 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
33 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
34 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
34 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
35 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
35 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
36 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
36 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
37 pyroutes.register('changelog_elements', '/%(repo_name)s/changelog_details', ['repo_name']);
37 pyroutes.register('changelog_elements', '/%(repo_name)s/changelog_details', ['repo_name']);
38 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
38 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
39 pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
39 pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
40 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
40 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
41 pyroutes.register('files_annotate_home', '/%(repo_name)s/annotate/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
41 pyroutes.register('files_annotate_home', '/%(repo_name)s/annotate/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
42 pyroutes.register('files_annotate_previous', '/%(repo_name)s/annotate-previous/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
42 pyroutes.register('files_annotate_previous', '/%(repo_name)s/annotate-previous/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
43 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
43 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
44 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
44 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
45 pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
45 pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
46 pyroutes.register('favicon', '/favicon.ico', []);
46 pyroutes.register('favicon', '/favicon.ico', []);
47 pyroutes.register('robots', '/robots.txt', []);
47 pyroutes.register('robots', '/robots.txt', []);
48 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
48 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
49 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
49 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
50 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
50 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
51 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
51 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
52 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
52 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
53 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
53 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
54 pyroutes.register('repo_group_integrations_home', '%(repo_group_name)s/settings/integrations', ['repo_group_name']);
54 pyroutes.register('repo_group_integrations_home', '%(repo_group_name)s/settings/integrations', ['repo_group_name']);
55 pyroutes.register('repo_group_integrations_list', '%(repo_group_name)s/settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
55 pyroutes.register('repo_group_integrations_list', '%(repo_group_name)s/settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
56 pyroutes.register('repo_group_integrations_new', '%(repo_group_name)s/settings/integrations/new', ['repo_group_name']);
56 pyroutes.register('repo_group_integrations_new', '%(repo_group_name)s/settings/integrations/new', ['repo_group_name']);
57 pyroutes.register('repo_group_integrations_create', '%(repo_group_name)s/settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
57 pyroutes.register('repo_group_integrations_create', '%(repo_group_name)s/settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
58 pyroutes.register('repo_group_integrations_edit', '%(repo_group_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
58 pyroutes.register('repo_group_integrations_edit', '%(repo_group_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
59 pyroutes.register('repo_integrations_home', '%(repo_name)s/settings/integrations', ['repo_name']);
59 pyroutes.register('repo_integrations_home', '%(repo_name)s/settings/integrations', ['repo_name']);
60 pyroutes.register('repo_integrations_list', '%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
60 pyroutes.register('repo_integrations_list', '%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
61 pyroutes.register('repo_integrations_new', '%(repo_name)s/settings/integrations/new', ['repo_name']);
61 pyroutes.register('repo_integrations_new', '%(repo_name)s/settings/integrations/new', ['repo_name']);
62 pyroutes.register('repo_integrations_create', '%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
62 pyroutes.register('repo_integrations_create', '%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
63 pyroutes.register('repo_integrations_edit', '%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
63 pyroutes.register('repo_integrations_edit', '%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
64 pyroutes.register('ops_ping', '_admin/ops/ping', []);
64 pyroutes.register('ops_ping', '_admin/ops/ping', []);
65 pyroutes.register('admin_home', '/_admin', []);
65 pyroutes.register('admin_home', '/_admin', []);
66 pyroutes.register('admin_audit_logs', '_admin/audit_logs', []);
66 pyroutes.register('admin_audit_logs', '_admin/audit_logs', []);
67 pyroutes.register('pull_requests_global_0', '_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
67 pyroutes.register('pull_requests_global_0', '_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
68 pyroutes.register('pull_requests_global_1', '_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
68 pyroutes.register('pull_requests_global_1', '_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
69 pyroutes.register('pull_requests_global', '_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
69 pyroutes.register('pull_requests_global', '_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
70 pyroutes.register('admin_settings_open_source', '_admin/settings/open_source', []);
70 pyroutes.register('admin_settings_open_source', '_admin/settings/open_source', []);
71 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '_admin/settings/vcs/svn_generate_cfg', []);
71 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '_admin/settings/vcs/svn_generate_cfg', []);
72 pyroutes.register('admin_settings_system', '_admin/settings/system', []);
72 pyroutes.register('admin_settings_system', '_admin/settings/system', []);
73 pyroutes.register('admin_settings_system_update', '_admin/settings/system/updates', []);
73 pyroutes.register('admin_settings_system_update', '_admin/settings/system/updates', []);
74 pyroutes.register('admin_settings_sessions', '_admin/settings/sessions', []);
74 pyroutes.register('admin_settings_sessions', '_admin/settings/sessions', []);
75 pyroutes.register('admin_settings_sessions_cleanup', '_admin/settings/sessions/cleanup', []);
75 pyroutes.register('admin_settings_sessions_cleanup', '_admin/settings/sessions/cleanup', []);
76 pyroutes.register('users', '_admin/users', []);
76 pyroutes.register('users', '_admin/users', []);
77 pyroutes.register('users_data', '_admin/users_data', []);
77 pyroutes.register('users_data', '_admin/users_data', []);
78 pyroutes.register('edit_user_auth_tokens', '_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
78 pyroutes.register('edit_user_auth_tokens', '_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
79 pyroutes.register('edit_user_auth_tokens_add', '_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
79 pyroutes.register('edit_user_auth_tokens_add', '_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
80 pyroutes.register('edit_user_auth_tokens_delete', '_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
80 pyroutes.register('edit_user_auth_tokens_delete', '_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
81 pyroutes.register('edit_user_groups_management', '_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
81 pyroutes.register('edit_user_groups_management', '_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
82 pyroutes.register('edit_user_groups_management_updates', '_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
82 pyroutes.register('edit_user_groups_management_updates', '_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
83 pyroutes.register('edit_user_audit_logs', '_admin/users/%(user_id)s/edit/audit', ['user_id']);
83 pyroutes.register('edit_user_audit_logs', '_admin/users/%(user_id)s/edit/audit', ['user_id']);
84 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
84 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
85 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
85 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
86 pyroutes.register('channelstream_proxy', '/_channelstream', []);
86 pyroutes.register('channelstream_proxy', '/_channelstream', []);
87 pyroutes.register('login', '/_admin/login', []);
87 pyroutes.register('login', '/_admin/login', []);
88 pyroutes.register('logout', '/_admin/logout', []);
88 pyroutes.register('logout', '/_admin/logout', []);
89 pyroutes.register('register', '/_admin/register', []);
89 pyroutes.register('register', '/_admin/register', []);
90 pyroutes.register('reset_password', '/_admin/password_reset', []);
90 pyroutes.register('reset_password', '/_admin/password_reset', []);
91 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
91 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
92 pyroutes.register('home', '/', []);
92 pyroutes.register('home', '/', []);
93 pyroutes.register('user_autocomplete_data', '/_users', []);
93 pyroutes.register('user_autocomplete_data', '/_users', []);
94 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
94 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
95 pyroutes.register('repo_list_data', '/_repos', []);
95 pyroutes.register('repo_list_data', '/_repos', []);
96 pyroutes.register('goto_switcher_data', '/_goto_data', []);
96 pyroutes.register('goto_switcher_data', '/_goto_data', []);
97 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
97 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
98 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
98 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
99 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
99 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
100 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
100 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
101 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
101 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
102 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
102 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
103 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
103 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
104 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
104 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
105 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
105 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
106 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
106 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
107 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
107 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
108 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
108 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
109 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
109 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
110 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
110 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
111 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
111 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
112 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
112 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
113 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
113 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
114 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
114 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
115 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
115 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
116 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
116 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
117 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
117 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
118 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
118 pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
119 pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
119 pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
120 pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
120 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
121 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
121 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
122 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
122 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
123 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
123 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
124 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
124 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
125 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
125 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
126 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
126 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
127 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
127 pyroutes.register('search', '/_admin/search', []);
128 pyroutes.register('search', '/_admin/search', []);
128 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
129 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
129 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
130 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
130 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
131 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
131 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
132 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
132 pyroutes.register('my_account_password_update', '/_admin/my_account/password', []);
133 pyroutes.register('my_account_password_update', '/_admin/my_account/password', []);
133 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
134 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
134 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
135 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
135 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
136 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
136 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
137 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
137 pyroutes.register('apiv2', '/_admin/api', []);
138 pyroutes.register('apiv2', '/_admin/api', []);
138 }
139 }
@@ -1,615 +1,605 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 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
19
20 var prButtonLockChecks = {
20 var prButtonLockChecks = {
21 'compare': false,
21 'compare': false,
22 'reviewers': false
22 'reviewers': false
23 };
23 };
24
24
25 /**
25 /**
26 * lock button until all checks and loads are made. E.g reviewer calculation
26 * lock button until all checks and loads are made. E.g reviewer calculation
27 * should prevent from submitting a PR
27 * should prevent from submitting a PR
28 * @param lockEnabled
28 * @param lockEnabled
29 * @param msg
29 * @param msg
30 * @param scope
30 * @param scope
31 */
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
33 scope = scope || 'all';
34 if (scope == 'all'){
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
43 if (lockEnabled) {
44 $('#save').attr('disabled', 'disabled');
44 $('#save').attr('disabled', 'disabled');
45 }
45 }
46 else if (checksMeet) {
46 else if (checksMeet) {
47 $('#save').removeAttr('disabled');
47 $('#save').removeAttr('disabled');
48 }
48 }
49
49
50 if (msg) {
50 if (msg) {
51 $('#pr_open_message').html(msg);
51 $('#pr_open_message').html(msg);
52 }
52 }
53 };
53 };
54
54
55
55
56 /**
56 /**
57 Generate Title and Description for a PullRequest.
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
60 and build description in a form
61 - commitN
61 - commitN
62 - commitN+1
62 - commitN+1
63 ...
63 ...
64
64
65 Title is then constructed from branch names, or other references,
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
66 replacing '-' and '_' into spaces
67
67
68 * @param sourceRef
68 * @param sourceRef
69 * @param elements
69 * @param elements
70 * @param limit
70 * @param limit
71 * @returns {*[]}
71 * @returns {*[]}
72 */
72 */
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 var title = '';
74 var title = '';
75 var desc = '';
75 var desc = '';
76
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
80 });
81 // only 1 commit, use commit message as title
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
82 if (elements.length === 1) {
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 }
84 }
85 else {
85 else {
86 // use reference name
86 // use reference name
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 }
88 }
89
89
90 return [title, desc]
90 return [title, desc]
91 };
91 };
92
92
93
93
94
94
95 ReviewersController = function () {
95 ReviewersController = function () {
96 var self = this;
96 var self = this;
97 this.$reviewRulesContainer = $('#review_rules');
97 this.$reviewRulesContainer = $('#review_rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 this.forbidReviewUsers = undefined;
99 this.forbidReviewUsers = undefined;
100 this.$reviewMembers = $('#review_members');
100 this.$reviewMembers = $('#review_members');
101 this.currentRequest = null;
101 this.currentRequest = null;
102
102
103 this.defaultForbidReviewUsers = function() {
103 this.defaultForbidReviewUsers = function() {
104 return [
104 return [
105 {'username': 'default',
105 {'username': 'default',
106 'user_id': templateContext.default_user.user_id}
106 'user_id': templateContext.default_user.user_id}
107 ];
107 ];
108 };
108 };
109
109
110 this.hideReviewRules = function() {
110 this.hideReviewRules = function() {
111 self.$reviewRulesContainer.hide();
111 self.$reviewRulesContainer.hide();
112 };
112 };
113
113
114 this.showReviewRules = function() {
114 this.showReviewRules = function() {
115 self.$reviewRulesContainer.show();
115 self.$reviewRulesContainer.show();
116 };
116 };
117
117
118 this.addRule = function(ruleText) {
118 this.addRule = function(ruleText) {
119 self.showReviewRules();
119 self.showReviewRules();
120 return '<div>- {0}</div>'.format(ruleText)
120 return '<div>- {0}</div>'.format(ruleText)
121 };
121 };
122
122
123 this.loadReviewRules = function(data) {
123 this.loadReviewRules = function(data) {
124 // reset forbidden Users
124 // reset forbidden Users
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126
126
127 // reset state of review rules
127 // reset state of review rules
128 self.$rulesList.html('');
128 self.$rulesList.html('');
129
129
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 // default rule, case for older repo that don't have any rules stored
131 // default rule, case for older repo that don't have any rules stored
132 self.$rulesList.append(
132 self.$rulesList.append(
133 self.addRule(
133 self.addRule(
134 _gettext('All reviewers must vote.'))
134 _gettext('All reviewers must vote.'))
135 );
135 );
136 return self.forbidReviewUsers
136 return self.forbidReviewUsers
137 }
137 }
138
138
139 if (data.rules.voting !== undefined) {
139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0){
140 if (data.rules.voting < 0){
141 self.$rulesList.append(
141 self.$rulesList.append(
142 self.addRule(
142 self.addRule(
143 _gettext('All reviewers must vote.'))
143 _gettext('All reviewers must vote.'))
144 )
144 )
145 } else if (data.rules.voting === 1) {
145 } else if (data.rules.voting === 1) {
146 self.$rulesList.append(
146 self.$rulesList.append(
147 self.addRule(
147 self.addRule(
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 )
149 )
150
150
151 } else {
151 } else {
152 self.$rulesList.append(
152 self.$rulesList.append(
153 self.addRule(
153 self.addRule(
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 )
155 )
156 }
156 }
157 }
157 }
158 if (data.rules.use_code_authors_for_review) {
158 if (data.rules.use_code_authors_for_review) {
159 self.$rulesList.append(
159 self.$rulesList.append(
160 self.addRule(
160 self.addRule(
161 _gettext('Reviewers picked from source code changes.'))
161 _gettext('Reviewers picked from source code changes.'))
162 )
162 )
163 }
163 }
164 if (data.rules.forbid_adding_reviewers) {
164 if (data.rules.forbid_adding_reviewers) {
165 $('#add_reviewer_input').remove();
165 $('#add_reviewer_input').remove();
166 self.$rulesList.append(
166 self.$rulesList.append(
167 self.addRule(
167 self.addRule(
168 _gettext('Adding new reviewers is forbidden.'))
168 _gettext('Adding new reviewers is forbidden.'))
169 )
169 )
170 }
170 }
171 if (data.rules.forbid_author_to_review) {
171 if (data.rules.forbid_author_to_review) {
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
173 self.$rulesList.append(
173 self.$rulesList.append(
174 self.addRule(
174 self.addRule(
175 _gettext('Author is not allowed to be a reviewer.'))
175 _gettext('Author is not allowed to be a reviewer.'))
176 )
176 )
177 }
177 }
178 if (data.rules.forbid_commit_author_to_review) {
178 if (data.rules.forbid_commit_author_to_review) {
179
179
180 if (data.rules_data.forbidden_users) {
180 if (data.rules_data.forbidden_users) {
181 $.each(data.rules_data.forbidden_users, function(index, member_data) {
181 $.each(data.rules_data.forbidden_users, function(index, member_data) {
182 self.forbidReviewUsers.push(member_data)
182 self.forbidReviewUsers.push(member_data)
183 });
183 });
184
184
185 }
185 }
186
186
187 self.$rulesList.append(
187 self.$rulesList.append(
188 self.addRule(
188 self.addRule(
189 _gettext('Commit Authors are not allowed to be a reviewer.'))
189 _gettext('Commit Authors are not allowed to be a reviewer.'))
190 )
190 )
191 }
191 }
192
192
193 return self.forbidReviewUsers
193 return self.forbidReviewUsers
194 };
194 };
195
195
196 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
196 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
197
197
198 if (self.currentRequest) {
198 if (self.currentRequest) {
199 // make sure we cleanup old running requests before triggering this
199 // make sure we cleanup old running requests before triggering this
200 // again
200 // again
201 self.currentRequest.abort();
201 self.currentRequest.abort();
202 }
202 }
203
203
204 $('.calculate-reviewers').show();
204 $('.calculate-reviewers').show();
205 // reset reviewer members
205 // reset reviewer members
206 self.$reviewMembers.empty();
206 self.$reviewMembers.empty();
207
207
208 prButtonLock(true, null, 'reviewers');
208 prButtonLock(true, null, 'reviewers');
209 $('#user').hide(); // hide user autocomplete before load
209 $('#user').hide(); // hide user autocomplete before load
210
210
211 var url = pyroutes.url('repo_default_reviewers_data',
211 var url = pyroutes.url('repo_default_reviewers_data',
212 {
212 {
213 'repo_name': templateContext.repo_name,
213 'repo_name': templateContext.repo_name,
214 'source_repo': sourceRepo,
214 'source_repo': sourceRepo,
215 'source_ref': sourceRef[2],
215 'source_ref': sourceRef[2],
216 'target_repo': targetRepo,
216 'target_repo': targetRepo,
217 'target_ref': targetRef[2]
217 'target_ref': targetRef[2]
218 });
218 });
219
219
220 self.currentRequest = $.get(url)
220 self.currentRequest = $.get(url)
221 .done(function(data) {
221 .done(function(data) {
222 self.currentRequest = null;
222 self.currentRequest = null;
223
223
224 // review rules
224 // review rules
225 self.loadReviewRules(data);
225 self.loadReviewRules(data);
226
226
227 for (var i = 0; i < data.reviewers.length; i++) {
227 for (var i = 0; i < data.reviewers.length; i++) {
228 var reviewer = data.reviewers[i];
228 var reviewer = data.reviewers[i];
229 self.addReviewMember(
229 self.addReviewMember(
230 reviewer.user_id, reviewer.firstname,
230 reviewer.user_id, reviewer.firstname,
231 reviewer.lastname, reviewer.username,
231 reviewer.lastname, reviewer.username,
232 reviewer.gravatar_link, reviewer.reasons,
232 reviewer.gravatar_link, reviewer.reasons,
233 reviewer.mandatory);
233 reviewer.mandatory);
234 }
234 }
235 $('.calculate-reviewers').hide();
235 $('.calculate-reviewers').hide();
236 prButtonLock(false, null, 'reviewers');
236 prButtonLock(false, null, 'reviewers');
237 $('#user').show(); // show user autocomplete after load
237 $('#user').show(); // show user autocomplete after load
238 });
238 });
239 };
239 };
240
240
241 // check those, refactor
241 // check those, refactor
242 this.removeReviewMember = function(reviewer_id, mark_delete) {
242 this.removeReviewMember = function(reviewer_id, mark_delete) {
243 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
243 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
244
244
245 if(typeof(mark_delete) === undefined){
245 if(typeof(mark_delete) === undefined){
246 mark_delete = false;
246 mark_delete = false;
247 }
247 }
248
248
249 if(mark_delete === true){
249 if(mark_delete === true){
250 if (reviewer){
250 if (reviewer){
251 // now delete the input
251 // now delete the input
252 $('#reviewer_{0} input'.format(reviewer_id)).remove();
252 $('#reviewer_{0} input'.format(reviewer_id)).remove();
253 // mark as to-delete
253 // mark as to-delete
254 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
254 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
255 obj.addClass('to-delete');
255 obj.addClass('to-delete');
256 obj.css({"text-decoration":"line-through", "opacity": 0.5});
256 obj.css({"text-decoration":"line-through", "opacity": 0.5});
257 }
257 }
258 }
258 }
259 else{
259 else{
260 $('#reviewer_{0}'.format(reviewer_id)).remove();
260 $('#reviewer_{0}'.format(reviewer_id)).remove();
261 }
261 }
262 };
262 };
263
263
264 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
264 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
265 var members = self.$reviewMembers.get(0);
265 var members = self.$reviewMembers.get(0);
266 var reasons_html = '';
266 var reasons_html = '';
267 var reasons_inputs = '';
267 var reasons_inputs = '';
268 var reasons = reasons || [];
268 var reasons = reasons || [];
269 var mandatory = mandatory || false;
269 var mandatory = mandatory || false;
270
270
271 if (reasons) {
271 if (reasons) {
272 for (var i = 0; i < reasons.length; i++) {
272 for (var i = 0; i < reasons.length; i++) {
273 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
273 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
274 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
274 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
275 }
275 }
276 }
276 }
277 var tmpl = '' +
277 var tmpl = '' +
278 '<li id="reviewer_{2}" class="reviewer_entry">'+
278 '<li id="reviewer_{2}" class="reviewer_entry">'+
279 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
279 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
280 '<div class="reviewer_status">'+
280 '<div class="reviewer_status">'+
281 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
281 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
282 '</div>'+
282 '</div>'+
283 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
283 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
284 '<span class="reviewer_name user">{1}</span>'+
284 '<span class="reviewer_name user">{1}</span>'+
285 reasons_html +
285 reasons_html +
286 '<input type="hidden" name="user_id" value="{2}">'+
286 '<input type="hidden" name="user_id" value="{2}">'+
287 '<input type="hidden" name="__start__" value="reasons:sequence">'+
287 '<input type="hidden" name="__start__" value="reasons:sequence">'+
288 '{3}'+
288 '{3}'+
289 '<input type="hidden" name="__end__" value="reasons:sequence">';
289 '<input type="hidden" name="__end__" value="reasons:sequence">';
290
290
291 if (mandatory) {
291 if (mandatory) {
292 tmpl += ''+
292 tmpl += ''+
293 '<div class="reviewer_member_mandatory_remove">' +
293 '<div class="reviewer_member_mandatory_remove">' +
294 '<i class="icon-remove-sign"></i>'+
294 '<i class="icon-remove-sign"></i>'+
295 '</div>' +
295 '</div>' +
296 '<input type="hidden" name="mandatory" value="true">'+
296 '<input type="hidden" name="mandatory" value="true">'+
297 '<div class="reviewer_member_mandatory">' +
297 '<div class="reviewer_member_mandatory">' +
298 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
298 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
299 '</div>';
299 '</div>';
300
300
301 } else {
301 } else {
302 tmpl += ''+
302 tmpl += ''+
303 '<input type="hidden" name="mandatory" value="false">'+
303 '<input type="hidden" name="mandatory" value="false">'+
304 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
304 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
305 '<i class="icon-remove-sign"></i>'+
305 '<i class="icon-remove-sign"></i>'+
306 '</div>';
306 '</div>';
307 }
307 }
308 // continue template
308 // continue template
309 tmpl += ''+
309 tmpl += ''+
310 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
310 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
311 '</li>' ;
311 '</li>' ;
312
312
313 var displayname = "{0} ({1} {2})".format(
313 var displayname = "{0} ({1} {2})".format(
314 nname, escapeHtml(fname), escapeHtml(lname));
314 nname, escapeHtml(fname), escapeHtml(lname));
315 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
315 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
316 // check if we don't have this ID already in
316 // check if we don't have this ID already in
317 var ids = [];
317 var ids = [];
318 var _els = self.$reviewMembers.find('li').toArray();
318 var _els = self.$reviewMembers.find('li').toArray();
319 for (el in _els){
319 for (el in _els){
320 ids.push(_els[el].id)
320 ids.push(_els[el].id)
321 }
321 }
322
322
323 var userAllowedReview = function(userId) {
323 var userAllowedReview = function(userId) {
324 var allowed = true;
324 var allowed = true;
325 $.each(self.forbidReviewUsers, function(index, member_data) {
325 $.each(self.forbidReviewUsers, function(index, member_data) {
326 if (parseInt(userId) === member_data['user_id']) {
326 if (parseInt(userId) === member_data['user_id']) {
327 allowed = false;
327 allowed = false;
328 return false // breaks the loop
328 return false // breaks the loop
329 }
329 }
330 });
330 });
331 return allowed
331 return allowed
332 };
332 };
333
333
334 var userAllowed = userAllowedReview(id);
334 var userAllowed = userAllowedReview(id);
335 if (!userAllowed){
335 if (!userAllowed){
336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
337 }
337 }
338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
339
339
340 if(shouldAdd) {
340 if(shouldAdd) {
341 // only add if it's not there
341 // only add if it's not there
342 members.innerHTML += element;
342 members.innerHTML += element;
343 }
343 }
344
344
345 };
345 };
346
346
347 this.updateReviewers = function(repo_name, pull_request_id){
347 this.updateReviewers = function(repo_name, pull_request_id){
348 var postData = '_method=put&' + $('#reviewers input').serialize();
348 var postData = '_method=put&' + $('#reviewers input').serialize();
349 _updatePullRequest(repo_name, pull_request_id, postData);
349 _updatePullRequest(repo_name, pull_request_id, postData);
350 };
350 };
351
351
352 };
352 };
353
353
354
354
355 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
355 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
356 var url = pyroutes.url(
356 var url = pyroutes.url(
357 'pullrequest_update',
357 'pullrequest_update',
358 {"repo_name": repo_name, "pull_request_id": pull_request_id});
358 {"repo_name": repo_name, "pull_request_id": pull_request_id});
359 if (typeof postData === 'string' ) {
359 if (typeof postData === 'string' ) {
360 postData += '&csrf_token=' + CSRF_TOKEN;
360 postData += '&csrf_token=' + CSRF_TOKEN;
361 } else {
361 } else {
362 postData.csrf_token = CSRF_TOKEN;
362 postData.csrf_token = CSRF_TOKEN;
363 }
363 }
364 var success = function(o) {
364 var success = function(o) {
365 window.location.reload();
365 window.location.reload();
366 };
366 };
367 ajaxPOST(url, postData, success);
367 ajaxPOST(url, postData, success);
368 };
368 };
369
369
370 /**
370 /**
371 * PULL REQUEST reject & close
372 */
373 var closePullRequest = function(repo_name, pull_request_id) {
374 var postData = {
375 '_method': 'put',
376 'close_pull_request': true};
377 _updatePullRequest(repo_name, pull_request_id, postData);
378 };
379
380 /**
381 * PULL REQUEST update commits
371 * PULL REQUEST update commits
382 */
372 */
383 var updateCommits = function(repo_name, pull_request_id) {
373 var updateCommits = function(repo_name, pull_request_id) {
384 var postData = {
374 var postData = {
385 '_method': 'put',
375 '_method': 'put',
386 'update_commits': true};
376 'update_commits': true};
387 _updatePullRequest(repo_name, pull_request_id, postData);
377 _updatePullRequest(repo_name, pull_request_id, postData);
388 };
378 };
389
379
390
380
391 /**
381 /**
392 * PULL REQUEST edit info
382 * PULL REQUEST edit info
393 */
383 */
394 var editPullRequest = function(repo_name, pull_request_id, title, description) {
384 var editPullRequest = function(repo_name, pull_request_id, title, description) {
395 var url = pyroutes.url(
385 var url = pyroutes.url(
396 'pullrequest_update',
386 'pullrequest_update',
397 {"repo_name": repo_name, "pull_request_id": pull_request_id});
387 {"repo_name": repo_name, "pull_request_id": pull_request_id});
398
388
399 var postData = {
389 var postData = {
400 '_method': 'put',
390 '_method': 'put',
401 'title': title,
391 'title': title,
402 'description': description,
392 'description': description,
403 'edit_pull_request': true,
393 'edit_pull_request': true,
404 'csrf_token': CSRF_TOKEN
394 'csrf_token': CSRF_TOKEN
405 };
395 };
406 var success = function(o) {
396 var success = function(o) {
407 window.location.reload();
397 window.location.reload();
408 };
398 };
409 ajaxPOST(url, postData, success);
399 ajaxPOST(url, postData, success);
410 };
400 };
411
401
412 var initPullRequestsCodeMirror = function (textAreaId) {
402 var initPullRequestsCodeMirror = function (textAreaId) {
413 var ta = $(textAreaId).get(0);
403 var ta = $(textAreaId).get(0);
414 var initialHeight = '100px';
404 var initialHeight = '100px';
415
405
416 // default options
406 // default options
417 var codeMirrorOptions = {
407 var codeMirrorOptions = {
418 mode: "text",
408 mode: "text",
419 lineNumbers: false,
409 lineNumbers: false,
420 indentUnit: 4,
410 indentUnit: 4,
421 theme: 'rc-input'
411 theme: 'rc-input'
422 };
412 };
423
413
424 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
414 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
425 // marker for manually set description
415 // marker for manually set description
426 codeMirrorInstance._userDefinedDesc = false;
416 codeMirrorInstance._userDefinedDesc = false;
427 codeMirrorInstance.setSize(null, initialHeight);
417 codeMirrorInstance.setSize(null, initialHeight);
428 codeMirrorInstance.on("change", function(instance, changeObj) {
418 codeMirrorInstance.on("change", function(instance, changeObj) {
429 var height = initialHeight;
419 var height = initialHeight;
430 var lines = instance.lineCount();
420 var lines = instance.lineCount();
431 if (lines > 6 && lines < 20) {
421 if (lines > 6 && lines < 20) {
432 height = "auto"
422 height = "auto"
433 }
423 }
434 else if (lines >= 20) {
424 else if (lines >= 20) {
435 height = 20 * 15;
425 height = 20 * 15;
436 }
426 }
437 instance.setSize(null, height);
427 instance.setSize(null, height);
438
428
439 // detect if the change was trigger by auto desc, or user input
429 // detect if the change was trigger by auto desc, or user input
440 changeOrigin = changeObj.origin;
430 changeOrigin = changeObj.origin;
441
431
442 if (changeOrigin === "setValue") {
432 if (changeOrigin === "setValue") {
443 cmLog.debug('Change triggered by setValue');
433 cmLog.debug('Change triggered by setValue');
444 }
434 }
445 else {
435 else {
446 cmLog.debug('user triggered change !');
436 cmLog.debug('user triggered change !');
447 // set special marker to indicate user has created an input.
437 // set special marker to indicate user has created an input.
448 instance._userDefinedDesc = true;
438 instance._userDefinedDesc = true;
449 }
439 }
450
440
451 });
441 });
452
442
453 return codeMirrorInstance
443 return codeMirrorInstance
454 };
444 };
455
445
456 /**
446 /**
457 * Reviewer autocomplete
447 * Reviewer autocomplete
458 */
448 */
459 var ReviewerAutoComplete = function(inputId) {
449 var ReviewerAutoComplete = function(inputId) {
460 $(inputId).autocomplete({
450 $(inputId).autocomplete({
461 serviceUrl: pyroutes.url('user_autocomplete_data'),
451 serviceUrl: pyroutes.url('user_autocomplete_data'),
462 minChars:2,
452 minChars:2,
463 maxHeight:400,
453 maxHeight:400,
464 deferRequestBy: 300, //miliseconds
454 deferRequestBy: 300, //miliseconds
465 showNoSuggestionNotice: true,
455 showNoSuggestionNotice: true,
466 tabDisabled: true,
456 tabDisabled: true,
467 autoSelectFirst: true,
457 autoSelectFirst: true,
468 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
458 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
469 formatResult: autocompleteFormatResult,
459 formatResult: autocompleteFormatResult,
470 lookupFilter: autocompleteFilterResult,
460 lookupFilter: autocompleteFilterResult,
471 onSelect: function(element, data) {
461 onSelect: function(element, data) {
472
462
473 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
463 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
474 if (data.value_type == 'user_group') {
464 if (data.value_type == 'user_group') {
475 reasons.push(_gettext('member of "{0}"').format(data.value_display));
465 reasons.push(_gettext('member of "{0}"').format(data.value_display));
476
466
477 $.each(data.members, function(index, member_data) {
467 $.each(data.members, function(index, member_data) {
478 reviewersController.addReviewMember(
468 reviewersController.addReviewMember(
479 member_data.id, member_data.first_name, member_data.last_name,
469 member_data.id, member_data.first_name, member_data.last_name,
480 member_data.username, member_data.icon_link, reasons);
470 member_data.username, member_data.icon_link, reasons);
481 })
471 })
482
472
483 } else {
473 } else {
484 reviewersController.addReviewMember(
474 reviewersController.addReviewMember(
485 data.id, data.first_name, data.last_name,
475 data.id, data.first_name, data.last_name,
486 data.username, data.icon_link, reasons);
476 data.username, data.icon_link, reasons);
487 }
477 }
488
478
489 $(inputId).val('');
479 $(inputId).val('');
490 }
480 }
491 });
481 });
492 };
482 };
493
483
494
484
495 VersionController = function () {
485 VersionController = function () {
496 var self = this;
486 var self = this;
497 this.$verSource = $('input[name=ver_source]');
487 this.$verSource = $('input[name=ver_source]');
498 this.$verTarget = $('input[name=ver_target]');
488 this.$verTarget = $('input[name=ver_target]');
499 this.$showVersionDiff = $('#show-version-diff');
489 this.$showVersionDiff = $('#show-version-diff');
500
490
501 this.adjustRadioSelectors = function (curNode) {
491 this.adjustRadioSelectors = function (curNode) {
502 var getVal = function (item) {
492 var getVal = function (item) {
503 if (item == 'latest') {
493 if (item == 'latest') {
504 return Number.MAX_SAFE_INTEGER
494 return Number.MAX_SAFE_INTEGER
505 }
495 }
506 else {
496 else {
507 return parseInt(item)
497 return parseInt(item)
508 }
498 }
509 };
499 };
510
500
511 var curVal = getVal($(curNode).val());
501 var curVal = getVal($(curNode).val());
512 var cleared = false;
502 var cleared = false;
513
503
514 $.each(self.$verSource, function (index, value) {
504 $.each(self.$verSource, function (index, value) {
515 var elVal = getVal($(value).val());
505 var elVal = getVal($(value).val());
516
506
517 if (elVal > curVal) {
507 if (elVal > curVal) {
518 if ($(value).is(':checked')) {
508 if ($(value).is(':checked')) {
519 cleared = true;
509 cleared = true;
520 }
510 }
521 $(value).attr('disabled', 'disabled');
511 $(value).attr('disabled', 'disabled');
522 $(value).removeAttr('checked');
512 $(value).removeAttr('checked');
523 $(value).css({'opacity': 0.1});
513 $(value).css({'opacity': 0.1});
524 }
514 }
525 else {
515 else {
526 $(value).css({'opacity': 1});
516 $(value).css({'opacity': 1});
527 $(value).removeAttr('disabled');
517 $(value).removeAttr('disabled');
528 }
518 }
529 });
519 });
530
520
531 if (cleared) {
521 if (cleared) {
532 // if we unchecked an active, set the next one to same loc.
522 // if we unchecked an active, set the next one to same loc.
533 $(this.$verSource).filter('[value={0}]'.format(
523 $(this.$verSource).filter('[value={0}]'.format(
534 curVal)).attr('checked', 'checked');
524 curVal)).attr('checked', 'checked');
535 }
525 }
536
526
537 self.setLockAction(false,
527 self.setLockAction(false,
538 $(curNode).data('verPos'),
528 $(curNode).data('verPos'),
539 $(this.$verSource).filter(':checked').data('verPos')
529 $(this.$verSource).filter(':checked').data('verPos')
540 );
530 );
541 };
531 };
542
532
543
533
544 this.attachVersionListener = function () {
534 this.attachVersionListener = function () {
545 self.$verTarget.change(function (e) {
535 self.$verTarget.change(function (e) {
546 self.adjustRadioSelectors(this)
536 self.adjustRadioSelectors(this)
547 });
537 });
548 self.$verSource.change(function (e) {
538 self.$verSource.change(function (e) {
549 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
539 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
550 });
540 });
551 };
541 };
552
542
553 this.init = function () {
543 this.init = function () {
554
544
555 var curNode = self.$verTarget.filter(':checked');
545 var curNode = self.$verTarget.filter(':checked');
556 self.adjustRadioSelectors(curNode);
546 self.adjustRadioSelectors(curNode);
557 self.setLockAction(true);
547 self.setLockAction(true);
558 self.attachVersionListener();
548 self.attachVersionListener();
559
549
560 };
550 };
561
551
562 this.setLockAction = function (state, selectedVersion, otherVersion) {
552 this.setLockAction = function (state, selectedVersion, otherVersion) {
563 var $showVersionDiff = this.$showVersionDiff;
553 var $showVersionDiff = this.$showVersionDiff;
564
554
565 if (state) {
555 if (state) {
566 $showVersionDiff.attr('disabled', 'disabled');
556 $showVersionDiff.attr('disabled', 'disabled');
567 $showVersionDiff.addClass('disabled');
557 $showVersionDiff.addClass('disabled');
568 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
558 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
569 }
559 }
570 else {
560 else {
571 $showVersionDiff.removeAttr('disabled');
561 $showVersionDiff.removeAttr('disabled');
572 $showVersionDiff.removeClass('disabled');
562 $showVersionDiff.removeClass('disabled');
573
563
574 if (selectedVersion == otherVersion) {
564 if (selectedVersion == otherVersion) {
575 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
565 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
576 } else {
566 } else {
577 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
567 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
578 }
568 }
579 }
569 }
580
570
581 };
571 };
582
572
583 this.showVersionDiff = function () {
573 this.showVersionDiff = function () {
584 var target = self.$verTarget.filter(':checked');
574 var target = self.$verTarget.filter(':checked');
585 var source = self.$verSource.filter(':checked');
575 var source = self.$verSource.filter(':checked');
586
576
587 if (target.val() && source.val()) {
577 if (target.val() && source.val()) {
588 var params = {
578 var params = {
589 'pull_request_id': templateContext.pull_request_data.pull_request_id,
579 'pull_request_id': templateContext.pull_request_data.pull_request_id,
590 'repo_name': templateContext.repo_name,
580 'repo_name': templateContext.repo_name,
591 'version': target.val(),
581 'version': target.val(),
592 'from_version': source.val()
582 'from_version': source.val()
593 };
583 };
594 window.location = pyroutes.url('pullrequest_show', params)
584 window.location = pyroutes.url('pullrequest_show', params)
595 }
585 }
596
586
597 return false;
587 return false;
598 };
588 };
599
589
600 this.toggleVersionView = function (elem) {
590 this.toggleVersionView = function (elem) {
601
591
602 if (this.$showVersionDiff.is(':visible')) {
592 if (this.$showVersionDiff.is(':visible')) {
603 $('.version-pr').hide();
593 $('.version-pr').hide();
604 this.$showVersionDiff.hide();
594 this.$showVersionDiff.hide();
605 $(elem).html($(elem).data('toggleOn'))
595 $(elem).html($(elem).data('toggleOn'))
606 } else {
596 } else {
607 $('.version-pr').show();
597 $('.version-pr').show();
608 this.$showVersionDiff.show();
598 this.$showVersionDiff.show();
609 $(elem).html($(elem).data('toggleOff'))
599 $(elem).html($(elem).data('toggleOff'))
610 }
600 }
611
601
612 return false
602 return false
613 }
603 }
614
604
615 }; No newline at end of file
605 };
@@ -1,4 +1,4 b''
1 ## this is a dummy html file for partial rendering on server and sending
1 ## this is a dummy html file for partial rendering on server and sending
2 ## generated output via ajax after comment submit
2 ## generated output via ajax after comment submit
3 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ${comment.comment_block(c.co, inline=c.inline_comment)}
4 ${comment.comment_block(c.co, inline=c.co.is_inline)}
@@ -1,964 +1,961 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/debug_style/index.html"/>
2 <%inherit file="/debug_style/index.html"/>
3
3
4 <%def name="breadcrumbs_links()">
4 <%def name="breadcrumbs_links()">
5 ${h.link_to(_('Style'), h.url('debug_style_home'))}
5 ${h.link_to(_('Style'), h.url('debug_style_home'))}
6 &raquo;
6 &raquo;
7 ${c.active}
7 ${c.active}
8 </%def>
8 </%def>
9
9
10
10
11 <%def name="real_main()">
11 <%def name="real_main()">
12 <div class="box">
12 <div class="box">
13 <div class="title">
13 <div class="title">
14 ${self.breadcrumbs()}
14 ${self.breadcrumbs()}
15 </div>
15 </div>
16
16
17 <div class='sidebar-col-wrapper'>
17 <div class='sidebar-col-wrapper'>
18 ${self.sidebar()}
18 ${self.sidebar()}
19
19
20 <div class="main-content">
20 <div class="main-content">
21
21
22 <h2>Collapsable Content</h2>
22 <h2>Collapsable Content</h2>
23 <p>Where a section may have a very long list of information, it can be desirable to use collapsable content. There is a premade function for showing/hiding elements, though its use may or may not be practical, depending on the situation. Use it, or don't, on a case-by-case basis.</p>
23 <p>Where a section may have a very long list of information, it can be desirable to use collapsable content. There is a premade function for showing/hiding elements, though its use may or may not be practical, depending on the situation. Use it, or don't, on a case-by-case basis.</p>
24
24
25 <p><strong>To use the collapsable-content function:</strong> Create a toggle button using <code>&lt;div class="btn-collapse"&gt;Show More&lt;/div&gt;</code> and a data attribute using <code>data-toggle</code>. Clicking this button will toggle any sibling element(s) containing the class <code>collapsable-content</code> and an identical <code>data-toggle</code> attribute. It will also change the button to read "Show Less"; another click toggles it back to the previous state. Ideally, use pre-existing elements and add the class and attribute; creating a new div around the existing content may lead to unexpected results, as the toggle function will use <code>display:block</code> if no previous display specification was found.
25 <p><strong>To use the collapsable-content function:</strong> Create a toggle button using <code>&lt;div class="btn-collapse"&gt;Show More&lt;/div&gt;</code> and a data attribute using <code>data-toggle</code>. Clicking this button will toggle any sibling element(s) containing the class <code>collapsable-content</code> and an identical <code>data-toggle</code> attribute. It will also change the button to read "Show Less"; another click toggles it back to the previous state. Ideally, use pre-existing elements and add the class and attribute; creating a new div around the existing content may lead to unexpected results, as the toggle function will use <code>display:block</code> if no previous display specification was found.
26 </p>
26 </p>
27 <p>Notes:</p>
27 <p>Notes:</p>
28 <ul>
28 <ul>
29 <li>Changes made to the text of the button will require adjustment to the function, but for the sake of consistency and user experience, this is best avoided. </li>
29 <li>Changes made to the text of the button will require adjustment to the function, but for the sake of consistency and user experience, this is best avoided. </li>
30 <li>Collapsable content inside of a pjax loaded container will require <code>collapsableContent();</code> to be called from within the container. No variables are necessary.</li>
30 <li>Collapsable content inside of a pjax loaded container will require <code>collapsableContent();</code> to be called from within the container. No variables are necessary.</li>
31 </ul>
31 </ul>
32
32
33 </div> <!-- .main-content -->
33 </div> <!-- .main-content -->
34 </div> <!-- .sidebar-col-wrapper -->
34 </div> <!-- .sidebar-col-wrapper -->
35 </div> <!-- .box -->
35 </div> <!-- .box -->
36
36
37 <!-- CONTENT -->
37 <!-- CONTENT -->
38 <div id="content" class="wrapper">
38 <div id="content" class="wrapper">
39
39
40 <div class="main">
40 <div class="main">
41
41
42 <div class="box">
42 <div class="box">
43 <div class="title">
43 <div class="title">
44 <h1>
44 <h1>
45 Diff: enable filename with spaces on diffs
45 Diff: enable filename with spaces on diffs
46 </h1>
46 </h1>
47 <h1>
47 <h1>
48 <i class="icon-hg" ></i>
48 <i class="icon-hg" ></i>
49
49
50 <i class="icon-lock"></i>
50 <i class="icon-lock"></i>
51 <span><a href="/rhodecode-momentum">rhodecode-momentum</a></span>
51 <span><a href="/rhodecode-momentum">rhodecode-momentum</a></span>
52
52
53 </h1>
53 </h1>
54 </div>
54 </div>
55
55
56 <div class="box pr-summary">
56 <div class="box pr-summary">
57 <div class="summary-details block-left">
57 <div class="summary-details block-left">
58
58
59 <div class="pr-details-title">
59 <div class="pr-details-title">
60
60
61 Pull request #720 From Tue, 17 Feb 2015 16:21:38
61 Pull request #720 From Tue, 17 Feb 2015 16:21:38
62 <div class="btn-collapse" data-toggle="description">Show More</div>
62 <div class="btn-collapse" data-toggle="description">Show More</div>
63 </div>
63 </div>
64 <div id="summary" class="fields pr-details-content">
64 <div id="summary" class="fields pr-details-content">
65 <div class="field">
65 <div class="field">
66 <div class="label-summary">
66 <div class="label-summary">
67 <label>Origin:</label>
67 <label>Origin:</label>
68 </div>
68 </div>
69 <div class="input">
69 <div class="input">
70 <div>
70 <div>
71 <span class="tag">
71 <span class="tag">
72 <a href="/andersonsantos/rhodecode-momentum-fork#fix_574">book: fix_574</a>
72 <a href="/andersonsantos/rhodecode-momentum-fork#fix_574">book: fix_574</a>
73 </span>
73 </span>
74 <span class="clone-url">
74 <span class="clone-url">
75 <a href="/andersonsantos/rhodecode-momentum-fork">https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork</a>
75 <a href="/andersonsantos/rhodecode-momentum-fork">https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork</a>
76 </span>
76 </span>
77 </div>
77 </div>
78 <div>
78 <div>
79 <br>
79 <br>
80 <input type="text" value="hg pull -r 46b3d50315f0 https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork" readonly="readonly">
80 <input type="text" value="hg pull -r 46b3d50315f0 https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork" readonly="readonly">
81 </div>
81 </div>
82 </div>
82 </div>
83 </div>
83 </div>
84 <div class="field">
84 <div class="field">
85 <div class="label-summary">
85 <div class="label-summary">
86 <label>Review:</label>
86 <label>Review:</label>
87 </div>
87 </div>
88 <div class="input">
88 <div class="input">
89 <div class="flag_status under_review tooltip pull-left" title="Pull request status calculated from votes"></div>
89 <div class="flag_status under_review tooltip pull-left" title="Pull request status calculated from votes"></div>
90 <span class="changeset-status-lbl tooltip" title="Pull request status calculated from votes">
90 <span class="changeset-status-lbl tooltip" title="Pull request status calculated from votes">
91 Under Review
91 Under Review
92 </span>
92 </span>
93
93
94 </div>
94 </div>
95 </div>
95 </div>
96 <div class="field collapsable-content" data-toggle="description">
96 <div class="field collapsable-content" data-toggle="description">
97 <div class="label-summary">
97 <div class="label-summary">
98 <label>Description:</label>
98 <label>Description:</label>
99 </div>
99 </div>
100 <div class="input">
100 <div class="input">
101 <div class="pr-description">Fixing issue <a class="issue- tracker-link" href="http://bugs.rhodecode.com/issues/574"># 574</a>, changing regex for capturing filenames</div>
101 <div class="pr-description">Fixing issue <a class="issue- tracker-link" href="http://bugs.rhodecode.com/issues/574"># 574</a>, changing regex for capturing filenames</div>
102 </div>
102 </div>
103 </div>
103 </div>
104 <div class="field collapsable-content" data-toggle="description">
104 <div class="field collapsable-content" data-toggle="description">
105 <div class="label-summary">
105 <div class="label-summary">
106 <label>Comments:</label>
106 <label>Comments:</label>
107 </div>
107 </div>
108 <div class="input">
108 <div class="input">
109 <div>
109 <div>
110 <div class="comments-number">
110 <div class="comments-number">
111 <a href="#inline-comments-container">0 Pull request comments</a>,
111 <a href="#inline-comments-container">0 Pull request comments</a>,
112 0 Inline Comments
112 0 Inline Comments
113 </div>
113 </div>
114 </div>
114 </div>
115 </div>
115 </div>
116 </div>
116 </div>
117 </div>
117 </div>
118 </div>
118 </div>
119 <div>
119 <div>
120 <div class="reviewers-title block-right">
120 <div class="reviewers-title block-right">
121 <div class="pr-details-title">
121 <div class="pr-details-title">
122 Author
122 Author
123 </div>
123 </div>
124 </div>
124 </div>
125 <div class="block-right pr-details-content reviewers">
125 <div class="block-right pr-details-content reviewers">
126 <ul class="group_members">
126 <ul class="group_members">
127 <li>
127 <li>
128 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
128 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
129 <span class="user"> <a href="/_profiles/lolek">lolek (Lolek Santos)</a></span>
129 <span class="user"> <a href="/_profiles/lolek">lolek (Lolek Santos)</a></span>
130 </li>
130 </li>
131 </ul>
131 </ul>
132 </div>
132 </div>
133 <div class="reviewers-title block-right">
133 <div class="reviewers-title block-right">
134 <div class="pr-details-title">
134 <div class="pr-details-title">
135 Pull request reviewers
135 Pull request reviewers
136 <span class="btn-collapse" data-toggle="reviewers">Show More</span>
136 <span class="btn-collapse" data-toggle="reviewers">Show More</span>
137 </div>
137 </div>
138
138
139 </div>
139 </div>
140 <div id="reviewers" class="block-right pr-details-content reviewers">
140 <div id="reviewers" class="block-right pr-details-content reviewers">
141
141
142 <ul id="review_members" class="group_members">
142 <ul id="review_members" class="group_members">
143 <li id="reviewer_70">
143 <li id="reviewer_70">
144 <div class="reviewers_member">
144 <div class="reviewers_member">
145 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
145 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
146 <div class="flag_status rejected pull-left reviewer_member_status"></div>
146 <div class="flag_status rejected pull-left reviewer_member_status"></div>
147 </div>
147 </div>
148 <img class="gravatar" src="https://secure.gravatar.com/avatar/153a0fab13160b3e64a2cbc7c0373506?d=identicon&amp;s=32" height="16" width="16">
148 <img class="gravatar" src="https://secure.gravatar.com/avatar/153a0fab13160b3e64a2cbc7c0373506?d=identicon&amp;s=32" height="16" width="16">
149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
150 </div>
150 </div>
151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
152 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(70, true)" style="visibility: hidden;">
152 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(70, true)" style="visibility: hidden;">
153 <i class="icon-remove-sign"></i>
153 <i class="icon-remove-sign"></i>
154 </div>
154 </div>
155 </li>
155 </li>
156 <li id="reviewer_33" class="collapsable-content" data-toggle="reviewers">
156 <li id="reviewer_33" class="collapsable-content" data-toggle="reviewers">
157 <div class="reviewers_member">
157 <div class="reviewers_member">
158 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
158 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
159 <div class="flag_status approved pull-left reviewer_member_status"></div>
159 <div class="flag_status approved pull-left reviewer_member_status"></div>
160 </div>
160 </div>
161 <img class="gravatar" src="https://secure.gravatar.com/avatar/ffd6a317ec2b66be880143cd8459d0d9?d=identicon&amp;s=32" height="16" width="16">
161 <img class="gravatar" src="https://secure.gravatar.com/avatar/ffd6a317ec2b66be880143cd8459d0d9?d=identicon&amp;s=32" height="16" width="16">
162 <span class="user"> <a href="/_profiles/jenkins-tests">garbas (Rok Garbas)</a> (reviewer)</span>
162 <span class="user"> <a href="/_profiles/jenkins-tests">garbas (Rok Garbas)</a> (reviewer)</span>
163 </div>
163 </div>
164 </li>
164 </li>
165 <li id="reviewer_2" class="collapsable-content" data-toggle="reviewers">
165 <li id="reviewer_2" class="collapsable-content" data-toggle="reviewers">
166 <div class="reviewers_member">
166 <div class="reviewers_member">
167 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
167 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
168 <div class="flag_status not_reviewed pull-left reviewer_member_status"></div>
168 <div class="flag_status not_reviewed pull-left reviewer_member_status"></div>
169 </div>
169 </div>
170 <img class="gravatar" src="https://secure.gravatar.com/avatar/aad9d40cac1259ea39b5578554ad9d64?d=identicon&amp;s=32" height="16" width="16">
170 <img class="gravatar" src="https://secure.gravatar.com/avatar/aad9d40cac1259ea39b5578554ad9d64?d=identicon&amp;s=32" height="16" width="16">
171 <span class="user"> <a href="/_profiles/jenkins-tests">marcink (Marcin Kuzminski)</a> (reviewer)</span>
171 <span class="user"> <a href="/_profiles/jenkins-tests">marcink (Marcin Kuzminski)</a> (reviewer)</span>
172 </div>
172 </div>
173 </li>
173 </li>
174 <li id="reviewer_36" class="collapsable-content" data-toggle="reviewers">
174 <li id="reviewer_36" class="collapsable-content" data-toggle="reviewers">
175 <div class="reviewers_member">
175 <div class="reviewers_member">
176 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
176 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
177 <div class="flag_status approved pull-left reviewer_member_status"></div>
177 <div class="flag_status approved pull-left reviewer_member_status"></div>
178 </div>
178 </div>
179 <img class="gravatar" src="https://secure.gravatar.com/avatar/7a4da001a0af0016ed056ab523255db9?d=identicon&amp;s=32" height="16" width="16">
179 <img class="gravatar" src="https://secure.gravatar.com/avatar/7a4da001a0af0016ed056ab523255db9?d=identicon&amp;s=32" height="16" width="16">
180 <span class="user"> <a href="/_profiles/jenkins-tests">johbo (Johannes Bornhold)</a> (reviewer)</span>
180 <span class="user"> <a href="/_profiles/jenkins-tests">johbo (Johannes Bornhold)</a> (reviewer)</span>
181 </div>
181 </div>
182 </li>
182 </li>
183 <li id="reviewer_47" class="collapsable-content" data-toggle="reviewers">
183 <li id="reviewer_47" class="collapsable-content" data-toggle="reviewers">
184 <div class="reviewers_member">
184 <div class="reviewers_member">
185 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
185 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
186 <div class="flag_status under_review pull-left reviewer_member_status"></div>
186 <div class="flag_status under_review pull-left reviewer_member_status"></div>
187 </div>
187 </div>
188 <img class="gravatar" src="https://secure.gravatar.com/avatar/8f6dc00dce79d6bd7d415be5cea6a008?d=identicon&amp;s=32" height="16" width="16">
188 <img class="gravatar" src="https://secure.gravatar.com/avatar/8f6dc00dce79d6bd7d415be5cea6a008?d=identicon&amp;s=32" height="16" width="16">
189 <span class="user"> <a href="/_profiles/jenkins-tests">lisaq (Lisa Quatmann)</a> (reviewer)</span>
189 <span class="user"> <a href="/_profiles/jenkins-tests">lisaq (Lisa Quatmann)</a> (reviewer)</span>
190 </div>
190 </div>
191 </li>
191 </li>
192 <li id="reviewer_49" class="collapsable-content" data-toggle="reviewers">
192 <li id="reviewer_49" class="collapsable-content" data-toggle="reviewers">
193 <div class="reviewers_member">
193 <div class="reviewers_member">
194 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
194 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
195 <div class="flag_status approved pull-left reviewer_member_status"></div>
195 <div class="flag_status approved pull-left reviewer_member_status"></div>
196 </div>
196 </div>
197 <img class="gravatar" src="https://secure.gravatar.com/avatar/89f722927932a8f737a0feafb03a606e?d=identicon&amp;s=32" height="16" width="16">
197 <img class="gravatar" src="https://secure.gravatar.com/avatar/89f722927932a8f737a0feafb03a606e?d=identicon&amp;s=32" height="16" width="16">
198 <span class="user"> <a href="/_profiles/jenkins-tests">paris (Paris Kolios)</a> (reviewer)</span>
198 <span class="user"> <a href="/_profiles/jenkins-tests">paris (Paris Kolios)</a> (reviewer)</span>
199 </div>
199 </div>
200 </li>
200 </li>
201 <li id="reviewer_50" class="collapsable-content" data-toggle="reviewers">
201 <li id="reviewer_50" class="collapsable-content" data-toggle="reviewers">
202 <div class="reviewers_member">
202 <div class="reviewers_member">
203 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
203 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
204 <div class="flag_status approved pull-left reviewer_member_status"></div>
204 <div class="flag_status approved pull-left reviewer_member_status"></div>
205 </div>
205 </div>
206 <img class="gravatar" src="https://secure.gravatar.com/avatar/081322c975e8545ec269372405fbd016?d=identicon&amp;s=32" height="16" width="16">
206 <img class="gravatar" src="https://secure.gravatar.com/avatar/081322c975e8545ec269372405fbd016?d=identicon&amp;s=32" height="16" width="16">
207 <span class="user"> <a href="/_profiles/jenkins-tests">ergo (Marcin Lulek)</a> (reviewer)</span>
207 <span class="user"> <a href="/_profiles/jenkins-tests">ergo (Marcin Lulek)</a> (reviewer)</span>
208 </div>
208 </div>
209 </li>
209 </li>
210 <li id="reviewer_54" class="collapsable-content" data-toggle="reviewers">
210 <li id="reviewer_54" class="collapsable-content" data-toggle="reviewers">
211 <div class="reviewers_member">
211 <div class="reviewers_member">
212 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
212 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
213 <div class="flag_status under_review pull-left reviewer_member_status"></div>
213 <div class="flag_status under_review pull-left reviewer_member_status"></div>
214 </div>
214 </div>
215 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
215 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
216 <span class="user"> <a href="/_profiles/jenkins-tests">anderson (Anderson Santos)</a> (reviewer)</span>
216 <span class="user"> <a href="/_profiles/jenkins-tests">anderson (Anderson Santos)</a> (reviewer)</span>
217 </div>
217 </div>
218 </li>
218 </li>
219 <li id="reviewer_57" class="collapsable-content" data-toggle="reviewers">
219 <li id="reviewer_57" class="collapsable-content" data-toggle="reviewers">
220 <div class="reviewers_member">
220 <div class="reviewers_member">
221 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
221 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
222 <div class="flag_status approved pull-left reviewer_member_status"></div>
222 <div class="flag_status approved pull-left reviewer_member_status"></div>
223 </div>
223 </div>
224 <img class="gravatar" src="https://secure.gravatar.com/avatar/23e2ee8f5fd462cba8129a40cc1e896c?d=identicon&amp;s=32" height="16" width="16">
224 <img class="gravatar" src="https://secure.gravatar.com/avatar/23e2ee8f5fd462cba8129a40cc1e896c?d=identicon&amp;s=32" height="16" width="16">
225 <span class="user"> <a href="/_profiles/jenkins-tests">gmgauthier (Greg Gauthier)</a> (reviewer)</span>
225 <span class="user"> <a href="/_profiles/jenkins-tests">gmgauthier (Greg Gauthier)</a> (reviewer)</span>
226 </div>
226 </div>
227 </li>
227 </li>
228 <li id="reviewer_31" class="collapsable-content" data-toggle="reviewers">
228 <li id="reviewer_31" class="collapsable-content" data-toggle="reviewers">
229 <div class="reviewers_member">
229 <div class="reviewers_member">
230 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
230 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
231 <div class="flag_status under_review pull-left reviewer_member_status"></div>
231 <div class="flag_status under_review pull-left reviewer_member_status"></div>
232 </div>
232 </div>
233 <img class="gravatar" src="https://secure.gravatar.com/avatar/0c9a7e6674b6f0b35d98dbe073e3f0ab?d=identicon&amp;s=32" height="16" width="16">
233 <img class="gravatar" src="https://secure.gravatar.com/avatar/0c9a7e6674b6f0b35d98dbe073e3f0ab?d=identicon&amp;s=32" height="16" width="16">
234 <span class="user"> <a href="/_profiles/jenkins-tests">ostrobel (Oliver Strobel)</a> (reviewer)</span>
234 <span class="user"> <a href="/_profiles/jenkins-tests">ostrobel (Oliver Strobel)</a> (reviewer)</span>
235 </div>
235 </div>
236 </li>
236 </li>
237 </ul>
237 </ul>
238 <div id="add_reviewer_input" class="ac" style="display: none;">
238 <div id="add_reviewer_input" class="ac" style="display: none;">
239 </div>
239 </div>
240 </div>
240 </div>
241 </div>
241 </div>
242 </div>
242 </div>
243 </div>
243 </div>
244 <div class="box">
244 <div class="box">
245 <div class="table" >
245 <div class="table" >
246 <div id="changeset_compare_view_content">
246 <div id="changeset_compare_view_content">
247 <div class="compare_view_commits_title">
247 <div class="compare_view_commits_title">
248 <h2>Compare View: 6 commits<span class="btn-collapse" data-toggle="commits">Show More</span></h2>
248 <h2>Compare View: 6 commits<span class="btn-collapse" data-toggle="commits">Show More</span></h2>
249
249
250 </div>
250 </div>
251 <div class="container">
251 <div class="container">
252
252
253
253
254 <table class="rctable compare_view_commits">
254 <table class="rctable compare_view_commits">
255 <tr>
255 <tr>
256 <th>Time</th>
256 <th>Time</th>
257 <th>Author</th>
257 <th>Author</th>
258 <th>Commit</th>
258 <th>Commit</th>
259 <th></th>
259 <th></th>
260 <th>Title</th>
260 <th>Title</th>
261 </tr>
261 </tr>
262 <tr id="row-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" commit_id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="compare_select">
262 <tr id="row-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" commit_id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="compare_select">
263 <td class="td-time">
263 <td class="td-time">
264 <span class="tooltip" title="3 hours and 23 minutes ago" tt_title="3 hours and 23 minutes ago">2015-02-18 10:13:34</span>
264 <span class="tooltip" title="3 hours and 23 minutes ago" tt_title="3 hours and 23 minutes ago">2015-02-18 10:13:34</span>
265 </td>
265 </td>
266 <td class="td-user">
266 <td class="td-user">
267 <div class="gravatar_with_user">
267 <div class="gravatar_with_user">
268 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
268 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
269 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
269 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
270 </div>
270 </div>
271 </td>
271 </td>
272 <td class="td-hash">
272 <td class="td-hash">
273 <code>
273 <code>
274 <a href="/brian/documentation-rep/changeset/7e83e5cd7812dd9e055ce30e77c65cdc08154b43">r395:7e83e5cd7812</a>
274 <a href="/brian/documentation-rep/changeset/7e83e5cd7812dd9e055ce30e77c65cdc08154b43">r395:7e83e5cd7812</a>
275 </code>
275 </code>
276 </td>
276 </td>
277 <td class="expand_commit" data-commit-id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" title="Expand commit message">
277 <td class="expand_commit" data-commit-id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" title="Expand commit message">
278 <div class="show_more_col">
278 <div class="show_more_col">
279 <i class="show_more"></i>
279 <i class="show_more"></i>
280 </div>
280 </div>
281 </td>
281 </td>
282 <td class="mid td-description">
282 <td class="mid td-description">
283 <div class="log-container truncate-wrap">
283 <div class="log-container truncate-wrap">
284 <div id="c-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="message truncate">rep: added how we doc to guide</div>
284 <div id="c-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="message truncate">rep: added how we doc to guide</div>
285 </div>
285 </div>
286 </td>
286 </td>
287 </tr>
287 </tr>
288 <tr id="row-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" commit_id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="compare_select">
288 <tr id="row-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" commit_id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="compare_select">
289 <td class="td-time">
289 <td class="td-time">
290 <span class="tooltip" title="4 hours and 18 minutes ago">2015-02-18 09:18:31</span>
290 <span class="tooltip" title="4 hours and 18 minutes ago">2015-02-18 09:18:31</span>
291 </td>
291 </td>
292 <td class="td-user">
292 <td class="td-user">
293 <div class="gravatar_with_user">
293 <div class="gravatar_with_user">
294 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
294 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
295 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
295 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
296 </div>
296 </div>
297 </td>
297 </td>
298 <td class="td-hash">
298 <td class="td-hash">
299 <code>
299 <code>
300 <a href="/brian/documentation-rep/changeset/48ce1581bdb3aa7679c246cbdd3fb030623f5c87">r394:48ce1581bdb3</a>
300 <a href="/brian/documentation-rep/changeset/48ce1581bdb3aa7679c246cbdd3fb030623f5c87">r394:48ce1581bdb3</a>
301 </code>
301 </code>
302 </td>
302 </td>
303 <td class="expand_commit" data-commit-id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" title="Expand commit message">
303 <td class="expand_commit" data-commit-id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" title="Expand commit message">
304 <div class="show_more_col">
304 <div class="show_more_col">
305 <i class="show_more"></i>
305 <i class="show_more"></i>
306 </div>
306 </div>
307 </td>
307 </td>
308 <td class="mid td-description">
308 <td class="mid td-description">
309 <div class="log-container truncate-wrap">
309 <div class="log-container truncate-wrap">
310 <div id="c-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="message truncate">repo 0004 - typo</div>
310 <div id="c-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="message truncate">repo 0004 - typo</div>
311 </div>
311 </div>
312 </td>
312 </td>
313 </tr>
313 </tr>
314 <tr id="row-982d857aafb4c71e7686e419c32b71c9a837257d" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d" class="compare_select collapsable-content" data-toggle="commits">
314 <tr id="row-982d857aafb4c71e7686e419c32b71c9a837257d" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d" class="compare_select collapsable-content" data-toggle="commits">
315 <td class="td-time">
315 <td class="td-time">
316 <span class="tooltip" title="4 hours and 22 minutes ago">2015-02-18 09:14:45</span>
316 <span class="tooltip" title="4 hours and 22 minutes ago">2015-02-18 09:14:45</span>
317 </td>
317 </td>
318 <td class="td-user">
318 <td class="td-user">
319 <span class="gravatar" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d">
319 <span class="gravatar" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d">
320 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
320 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
321 </span>
321 </span>
322 <span class="author">brian (Brian Butler)</span>
322 <span class="author">brian (Brian Butler)</span>
323 </td>
323 </td>
324 <td class="td-hash">
324 <td class="td-hash">
325 <code>
325 <code>
326 <a href="/brian/documentation-rep/changeset/982d857aafb4c71e7686e419c32b71c9a837257d">r393:982d857aafb4</a>
326 <a href="/brian/documentation-rep/changeset/982d857aafb4c71e7686e419c32b71c9a837257d">r393:982d857aafb4</a>
327 </code>
327 </code>
328 </td>
328 </td>
329 <td class="expand_commit" data-commit-id="982d857aafb4c71e7686e419c32b71c9a837257d" title="Expand commit message">
329 <td class="expand_commit" data-commit-id="982d857aafb4c71e7686e419c32b71c9a837257d" title="Expand commit message">
330 <div class="show_more_col">
330 <div class="show_more_col">
331 <i class="show_more"></i>
331 <i class="show_more"></i>
332 </div>
332 </div>
333 </td>
333 </td>
334 <td class="mid td-description">
334 <td class="mid td-description">
335 <div class="log-container truncate-wrap">
335 <div class="log-container truncate-wrap">
336 <div id="c-982d857aafb4c71e7686e419c32b71c9a837257d" class="message truncate">internals: how to doc section added</div>
336 <div id="c-982d857aafb4c71e7686e419c32b71c9a837257d" class="message truncate">internals: how to doc section added</div>
337 </div>
337 </div>
338 </td>
338 </td>
339 </tr>
339 </tr>
340 <tr id="row-4c7258ad1af6dae91bbaf87a933e3597e676fab8" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="compare_select collapsable-content" data-toggle="commits">
340 <tr id="row-4c7258ad1af6dae91bbaf87a933e3597e676fab8" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="compare_select collapsable-content" data-toggle="commits">
341 <td class="td-time">
341 <td class="td-time">
342 <span class="tooltip" title="20 hours and 16 minutes ago">2015-02-17 17:20:44</span>
342 <span class="tooltip" title="20 hours and 16 minutes ago">2015-02-17 17:20:44</span>
343 </td>
343 </td>
344 <td class="td-user">
344 <td class="td-user">
345 <span class="gravatar" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8">
345 <span class="gravatar" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8">
346 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
346 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
347 </span>
347 </span>
348 <span class="author">brian (Brian Butler)</span>
348 <span class="author">brian (Brian Butler)</span>
349 </td>
349 </td>
350 <td class="td-hash">
350 <td class="td-hash">
351 <code>
351 <code>
352 <a href="/brian/documentation-rep/changeset/4c7258ad1af6dae91bbaf87a933e3597e676fab8">r392:4c7258ad1af6</a>
352 <a href="/brian/documentation-rep/changeset/4c7258ad1af6dae91bbaf87a933e3597e676fab8">r392:4c7258ad1af6</a>
353 </code>
353 </code>
354 </td>
354 </td>
355 <td class="expand_commit" data-commit-id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" title="Expand commit message">
355 <td class="expand_commit" data-commit-id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" title="Expand commit message">
356 <div class="show_more_col">
356 <div class="show_more_col">
357 <i class="show_more"></i>
357 <i class="show_more"></i>
358 </div>
358 </div>
359 </td>
359 </td>
360 <td class="mid td-description">
360 <td class="mid td-description">
361 <div class="log-container truncate-wrap">
361 <div class="log-container truncate-wrap">
362 <div id="c-4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="message truncate">REP: 0004 Documentation standards</div>
362 <div id="c-4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="message truncate">REP: 0004 Documentation standards</div>
363 </div>
363 </div>
364 </td>
364 </td>
365 </tr>
365 </tr>
366 <tr id="row-46b3d50315f0f2b1f64485ac95af4f384948f9cb" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="compare_select collapsable-content" data-toggle="commits">
366 <tr id="row-46b3d50315f0f2b1f64485ac95af4f384948f9cb" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="compare_select collapsable-content" data-toggle="commits">
367 <td class="td-time">
367 <td class="td-time">
368 <span class="tooltip" title="18 hours and 19 minutes ago">2015-02-17 16:18:49</span>
368 <span class="tooltip" title="18 hours and 19 minutes ago">2015-02-17 16:18:49</span>
369 </td>
369 </td>
370 <td class="td-user">
370 <td class="td-user">
371 <span class="gravatar" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb">
371 <span class="gravatar" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb">
372 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
372 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
373 </span>
373 </span>
374 <span class="author">anderson (Anderson Santos)</span>
374 <span class="author">anderson (Anderson Santos)</span>
375 </td>
375 </td>
376 <td class="td-hash">
376 <td class="td-hash">
377 <code>
377 <code>
378 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/46b3d50315f0f2b1f64485ac95af4f384948f9cb">r8743:46b3d50315f0</a>
378 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/46b3d50315f0f2b1f64485ac95af4f384948f9cb">r8743:46b3d50315f0</a>
379 </code>
379 </code>
380 </td>
380 </td>
381 <td class="expand_commit" data-commit-id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" title="Expand commit message">
381 <td class="expand_commit" data-commit-id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" title="Expand commit message">
382 <div class="show_more_col">
382 <div class="show_more_col">
383 <i class="show_more" ></i>
383 <i class="show_more" ></i>
384 </div>
384 </div>
385 </td>
385 </td>
386 <td class="mid td-description">
386 <td class="mid td-description">
387 <div class="log-container truncate-wrap">
387 <div class="log-container truncate-wrap">
388 <div id="c-46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="message truncate">Diff: created tests for the diff with filenames with spaces</div>
388 <div id="c-46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="message truncate">Diff: created tests for the diff with filenames with spaces</div>
389
389
390 </div>
390 </div>
391 </td>
391 </td>
392 </tr>
392 </tr>
393 <tr id="row-1e57d2549bd6c34798075bf05ac39f708bb33b90" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90" class="compare_select collapsable-content" data-toggle="commits">
393 <tr id="row-1e57d2549bd6c34798075bf05ac39f708bb33b90" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90" class="compare_select collapsable-content" data-toggle="commits">
394 <td class="td-time">
394 <td class="td-time">
395 <span class="tooltip" title="2 days ago">2015-02-16 10:06:08</span>
395 <span class="tooltip" title="2 days ago">2015-02-16 10:06:08</span>
396 </td>
396 </td>
397 <td class="td-user">
397 <td class="td-user">
398 <span class="gravatar" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90">
398 <span class="gravatar" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90">
399 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
399 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
400 </span>
400 </span>
401 <span class="author">anderson (Anderson Santos)</span>
401 <span class="author">anderson (Anderson Santos)</span>
402 </td>
402 </td>
403 <td class="td-hash">
403 <td class="td-hash">
404 <code>
404 <code>
405 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/1e57d2549bd6c34798075bf05ac39f708bb33b90">r8742:1e57d2549bd6</a>
405 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/1e57d2549bd6c34798075bf05ac39f708bb33b90">r8742:1e57d2549bd6</a>
406 </code>
406 </code>
407 </td>
407 </td>
408 <td class="expand_commit" data-commit-id="1e57d2549bd6c34798075bf05ac39f708bb33b90" title="Expand commit message">
408 <td class="expand_commit" data-commit-id="1e57d2549bd6c34798075bf05ac39f708bb33b90" title="Expand commit message">
409 <div class="show_more_col">
409 <div class="show_more_col">
410 <i class="show_more" ></i>
410 <i class="show_more" ></i>
411 </div>
411 </div>
412 </td>
412 </td>
413 <td class="mid td-description">
413 <td class="mid td-description">
414 <div class="log-container truncate-wrap">
414 <div class="log-container truncate-wrap">
415 <div id="c-1e57d2549bd6c34798075bf05ac39f708bb33b90" class="message truncate">Diff: fix renaming files with spaces <a class="issue-tracker-link" href="http://bugs.rhodecode.com/issues/574">#574</a></div>
415 <div id="c-1e57d2549bd6c34798075bf05ac39f708bb33b90" class="message truncate">Diff: fix renaming files with spaces <a class="issue-tracker-link" href="http://bugs.rhodecode.com/issues/574">#574</a></div>
416
416
417 </div>
417 </div>
418 </td>
418 </td>
419 </tr>
419 </tr>
420 </table>
420 </table>
421 </div>
421 </div>
422
422
423 <script>
423 <script>
424 $('.expand_commit').on('click',function(e){
424 $('.expand_commit').on('click',function(e){
425 $(this).children('i').hide();
425 $(this).children('i').hide();
426 var cid = $(this).data('commitId');
426 var cid = $(this).data('commitId');
427 $('#c-'+cid).css({'height': 'auto', 'margin': '.65em 1em .65em 0','white-space': 'pre-line', 'text-overflow': 'initial', 'overflow':'visible'})
427 $('#c-'+cid).css({'height': 'auto', 'margin': '.65em 1em .65em 0','white-space': 'pre-line', 'text-overflow': 'initial', 'overflow':'visible'})
428 $('#t-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible', 'white-space':'normal'})
428 $('#t-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible', 'white-space':'normal'})
429 });
429 });
430 $('.compare_select').on('click',function(e){
430 $('.compare_select').on('click',function(e){
431 var cid = $(this).attr('commit_id');
431 var cid = $(this).attr('commit_id');
432 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
432 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
433 });
433 });
434 </script>
434 </script>
435 <div class="cs_files_title">
435 <div class="cs_files_title">
436 <span class="cs_files_expand">
436 <span class="cs_files_expand">
437 <span id="expand_all_files">Expand All</span> | <span id="collapse_all_files">Collapse All</span>
437 <span id="expand_all_files">Expand All</span> | <span id="collapse_all_files">Collapse All</span>
438 </span>
438 </span>
439 <h2>
439 <h2>
440 7 files changed: 55 inserted, 9 deleted
440 7 files changed: 55 inserted, 9 deleted
441 </h2>
441 </h2>
442 </div>
442 </div>
443 <div class="cs_files">
443 <div class="cs_files">
444 <table class="compare_view_files">
444 <table class="compare_view_files">
445
445
446 <tr class="cs_A expand_file" fid="c--efbe5b7a3f13">
446 <tr class="cs_A expand_file" fid="c--efbe5b7a3f13">
447 <td class="cs_icon_td">
447 <td class="cs_icon_td">
448 <span class="expand_file_icon" fid="c--efbe5b7a3f13"></span>
448 <span class="expand_file_icon" fid="c--efbe5b7a3f13"></span>
449 </td>
449 </td>
450 <td class="cs_icon_td">
450 <td class="cs_icon_td">
451 <div class="flag_status not_reviewed hidden"></div>
451 <div class="flag_status not_reviewed hidden"></div>
452 </td>
452 </td>
453 <td id="a_c--efbe5b7a3f13">
453 <td id="a_c--efbe5b7a3f13">
454 <a class="compare_view_filepath" href="#a_c--efbe5b7a3f13">
454 <a class="compare_view_filepath" href="#a_c--efbe5b7a3f13">
455 rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff
455 rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff
456 </a>
456 </a>
457 <span id="diff_c--efbe5b7a3f13" class="diff_links" style="display: none;">
457 <span id="diff_c--efbe5b7a3f13" class="diff_links" style="display: none;">
458 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
458 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
459 Unified Diff
459 Unified Diff
460 </a>
460 </a>
461 |
461 |
462 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
462 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
463 Side-by-side Diff
463 Side-by-side Diff
464 </a>
464 </a>
465 </span>
465 </span>
466 </td>
466 </td>
467 <td>
467 <td>
468 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">4</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
468 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">4</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
469 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff">
469 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff">
470 <i class="icon-comment"></i>
470 <i class="icon-comment"></i>
471 </div>
471 </div>
472 </td>
472 </td>
473 </tr>
473 </tr>
474 <tr id="tr_c--efbe5b7a3f13">
474 <tr id="tr_c--efbe5b7a3f13">
475 <td></td>
475 <td></td>
476 <td></td>
476 <td></td>
477 <td class="injected_diff" colspan="2">
477 <td class="injected_diff" colspan="2">
478
478
479 <div class="diff-container" id="diff-container-140716195039928">
479 <div class="diff-container" id="diff-container-140716195039928">
480 <div id="c--efbe5b7a3f13_target" ></div>
480 <div id="c--efbe5b7a3f13_target" ></div>
481 <div id="c--efbe5b7a3f13" class="diffblock margined comm" >
481 <div id="c--efbe5b7a3f13" class="diffblock margined comm" >
482 <div class="code-body">
482 <div class="code-body">
483 <div class="full_f_path" path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff" style="display: none;"></div>
483 <div class="full_f_path" path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff" style="display: none;"></div>
484 <table class="code-difftable">
484 <table class="code-difftable">
485 <tr class="line context">
485 <tr class="line context">
486 <td class="add-comment-line"><span class="add-comment-content"></span></td>
486 <td class="add-comment-line"><span class="add-comment-content"></span></td>
487 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
487 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
488 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n"></a></td>
488 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n"></a></td>
489 <td class="code no-comment">
489 <td class="code no-comment">
490 <pre>new file 100644</pre>
490 <pre>new file 100644</pre>
491 </td>
491 </td>
492 </tr>
492 </tr>
493 <tr class="line add">
493 <tr class="line add">
494 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
494 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
495 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
495 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
496 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1">1</a></td>
496 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1">1</a></td>
497 <td class="code">
497 <td class="code">
498 <pre>diff --git a/file_with_ spaces.txt b/file_with_ two spaces.txt
498 <pre>diff --git a/file_with_ spaces.txt b/file_with_ two spaces.txt
499 </pre>
499 </pre>
500 </td>
500 </td>
501 </tr>
501 </tr>
502 <tr class="line add">
502 <tr class="line add">
503 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
503 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
504 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
504 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
505 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2">2</a></td>
505 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2">2</a></td>
506 <td class="code">
506 <td class="code">
507 <pre>similarity index 100%
507 <pre>similarity index 100%
508 </pre>
508 </pre>
509 </td>
509 </td>
510 </tr>
510 </tr>
511 <tr class="line add">
511 <tr class="line add">
512 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
512 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
513 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
513 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
514 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3">3</a></td>
514 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3">3</a></td>
515 <td class="code">
515 <td class="code">
516 <pre>rename from file_with_ spaces.txt
516 <pre>rename from file_with_ spaces.txt
517 </pre>
517 </pre>
518 </td>
518 </td>
519 </tr>
519 </tr>
520 <tr class="line add">
520 <tr class="line add">
521 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
521 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
522 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
522 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
523 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4">4</a></td>
523 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4">4</a></td>
524 <td class="code">
524 <td class="code">
525 <pre>rename to file_with_ two spaces.txt
525 <pre>rename to file_with_ two spaces.txt
526 </pre>
526 </pre>
527 </td>
527 </td>
528 </tr>
528 </tr>
529 <tr class="line context">
529 <tr class="line context">
530 <td class="add-comment-line"><span class="add-comment-content"></span></td>
530 <td class="add-comment-line"><span class="add-comment-content"></span></td>
531 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o...">...</a></td>
531 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o...">...</a></td>
532 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n...">...</a></td>
532 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n...">...</a></td>
533 <td class="code no-comment">
533 <td class="code no-comment">
534 <pre> No newline at end of file</pre>
534 <pre> No newline at end of file</pre>
535 </td>
535 </td>
536 </tr>
536 </tr>
537 </table>
537 </table>
538 </div>
538 </div>
539 </div>
539 </div>
540 </div>
540 </div>
541
541
542 </td>
542 </td>
543 </tr>
543 </tr>
544 <tr class="cs_A expand_file" fid="c--c21377f778f9">
544 <tr class="cs_A expand_file" fid="c--c21377f778f9">
545 <td class="cs_icon_td">
545 <td class="cs_icon_td">
546 <span class="expand_file_icon" fid="c--c21377f778f9"></span>
546 <span class="expand_file_icon" fid="c--c21377f778f9"></span>
547 </td>
547 </td>
548 <td class="cs_icon_td">
548 <td class="cs_icon_td">
549 <div class="flag_status not_reviewed hidden"></div>
549 <div class="flag_status not_reviewed hidden"></div>
550 </td>
550 </td>
551 <td id="a_c--c21377f778f9">
551 <td id="a_c--c21377f778f9">
552 <a class="compare_view_filepath" href="#a_c--c21377f778f9">
552 <a class="compare_view_filepath" href="#a_c--c21377f778f9">
553 rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff
553 rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff
554 </a>
554 </a>
555 <span id="diff_c--c21377f778f9" class="diff_links" style="display: none;">
555 <span id="diff_c--c21377f778f9" class="diff_links" style="display: none;">
556 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
556 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
557 Unified Diff
557 Unified Diff
558 </a>
558 </a>
559 |
559 |
560 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
560 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
561 Side-by-side Diff
561 Side-by-side Diff
562 </a>
562 </a>
563 </span>
563 </span>
564 </td>
564 </td>
565 <td>
565 <td>
566 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
566 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
567 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff">
567 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff">
568 <i class="icon-comment"></i>
568 <i class="icon-comment"></i>
569 </div>
569 </div>
570 </td>
570 </td>
571 </tr>
571 </tr>
572 <tr id="tr_c--c21377f778f9">
572 <tr id="tr_c--c21377f778f9">
573 <td></td>
573 <td></td>
574 <td></td>
574 <td></td>
575 <td class="injected_diff" colspan="2">
575 <td class="injected_diff" colspan="2">
576
576
577 <div class="diff-container" id="diff-container-140716195038344">
577 <div class="diff-container" id="diff-container-140716195038344">
578 <div id="c--c21377f778f9_target" ></div>
578 <div id="c--c21377f778f9_target" ></div>
579 <div id="c--c21377f778f9" class="diffblock margined comm" >
579 <div id="c--c21377f778f9" class="diffblock margined comm" >
580 <div class="code-body">
580 <div class="code-body">
581 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff" style="display: none;"></div>
581 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff" style="display: none;"></div>
582 <table class="code-difftable">
582 <table class="code-difftable">
583 <tr class="line context">
583 <tr class="line context">
584 <td class="add-comment-line"><span class="add-comment-content"></span></td>
584 <td class="add-comment-line"><span class="add-comment-content"></span></td>
585 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
585 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
586 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n"></a></td>
586 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n"></a></td>
587 <td class="code no-comment">
587 <td class="code no-comment">
588 <pre>new file 100644</pre>
588 <pre>new file 100644</pre>
589 </td>
589 </td>
590 </tr>
590 </tr>
591 <tr class="line add">
591 <tr class="line add">
592 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
592 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
593 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
593 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
594 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1">1</a></td>
594 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1">1</a></td>
595 <td class="code">
595 <td class="code">
596 <pre>diff --git a/file_changed_without_spaces.txt b/file_copied_ with spaces.txt
596 <pre>diff --git a/file_changed_without_spaces.txt b/file_copied_ with spaces.txt
597 </pre>
597 </pre>
598 </td>
598 </td>
599 </tr>
599 </tr>
600 <tr class="line add">
600 <tr class="line add">
601 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
601 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
602 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
602 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
603 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2">2</a></td>
603 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2">2</a></td>
604 <td class="code">
604 <td class="code">
605 <pre>copy from file_changed_without_spaces.txt
605 <pre>copy from file_changed_without_spaces.txt
606 </pre>
606 </pre>
607 </td>
607 </td>
608 </tr>
608 </tr>
609 <tr class="line add">
609 <tr class="line add">
610 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
610 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
611 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
611 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
612 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3">3</a></td>
612 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3">3</a></td>
613 <td class="code">
613 <td class="code">
614 <pre>copy to file_copied_ with spaces.txt
614 <pre>copy to file_copied_ with spaces.txt
615 </pre>
615 </pre>
616 </td>
616 </td>
617 </tr>
617 </tr>
618 <tr class="line context">
618 <tr class="line context">
619 <td class="add-comment-line"><span class="add-comment-content"></span></td>
619 <td class="add-comment-line"><span class="add-comment-content"></span></td>
620 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o...">...</a></td>
620 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o...">...</a></td>
621 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n...">...</a></td>
621 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n...">...</a></td>
622 <td class="code no-comment">
622 <td class="code no-comment">
623 <pre> No newline at end of file</pre>
623 <pre> No newline at end of file</pre>
624 </td>
624 </td>
625 </tr>
625 </tr>
626 </table>
626 </table>
627 </div>
627 </div>
628 </div>
628 </div>
629 </div>
629 </div>
630
630
631 </td>
631 </td>
632 </tr>
632 </tr>
633 <tr class="cs_A expand_file" fid="c--ee62085ad7a8">
633 <tr class="cs_A expand_file" fid="c--ee62085ad7a8">
634 <td class="cs_icon_td">
634 <td class="cs_icon_td">
635 <span class="expand_file_icon" fid="c--ee62085ad7a8"></span>
635 <span class="expand_file_icon" fid="c--ee62085ad7a8"></span>
636 </td>
636 </td>
637 <td class="cs_icon_td">
637 <td class="cs_icon_td">
638 <div class="flag_status not_reviewed hidden"></div>
638 <div class="flag_status not_reviewed hidden"></div>
639 </td>
639 </td>
640 <td id="a_c--ee62085ad7a8">
640 <td id="a_c--ee62085ad7a8">
641 <a class="compare_view_filepath" href="#a_c--ee62085ad7a8">
641 <a class="compare_view_filepath" href="#a_c--ee62085ad7a8">
642 rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff
642 rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff
643 </a>
643 </a>
644 <span id="diff_c--ee62085ad7a8" class="diff_links" style="display: none;">
644 <span id="diff_c--ee62085ad7a8" class="diff_links" style="display: none;">
645 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
645 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
646 Unified Diff
646 Unified Diff
647 </a>
647 </a>
648 |
648 |
649 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
649 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
650 Side-by-side Diff
650 Side-by-side Diff
651 </a>
651 </a>
652 </span>
652 </span>
653 </td>
653 </td>
654 <td>
654 <td>
655 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
655 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
656 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff">
656 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff">
657 <i class="icon-comment"></i>
657 <i class="icon-comment"></i>
658 </div>
658 </div>
659 </td>
659 </td>
660 </tr>
660 </tr>
661 <tr id="tr_c--ee62085ad7a8">
661 <tr id="tr_c--ee62085ad7a8">
662 <td></td>
662 <td></td>
663 <td></td>
663 <td></td>
664 <td class="injected_diff" colspan="2">
664 <td class="injected_diff" colspan="2">
665
665
666 <div class="diff-container" id="diff-container-140716195039496">
666 <div class="diff-container" id="diff-container-140716195039496">
667 <div id="c--ee62085ad7a8_target" ></div>
667 <div id="c--ee62085ad7a8_target" ></div>
668 <div id="c--ee62085ad7a8" class="diffblock margined comm" >
668 <div id="c--ee62085ad7a8" class="diffblock margined comm" >
669 <div class="code-body">
669 <div class="code-body">
670 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff" style="display: none;"></div>
670 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff" style="display: none;"></div>
671 <table class="code-difftable">
671 <table class="code-difftable">
672 <tr class="line context">
672 <tr class="line context">
673 <td class="add-comment-line"><span class="add-comment-content"></span></td>
673 <td class="add-comment-line"><span class="add-comment-content"></span></td>
674 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
674 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
675 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n"></a></td>
675 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n"></a></td>
676 <td class="code no-comment">
676 <td class="code no-comment">
677 <pre>new file 100644</pre>
677 <pre>new file 100644</pre>
678 </td>
678 </td>
679 </tr>
679 </tr>
680 <tr class="line add">
680 <tr class="line add">
681 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
681 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
682 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
682 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
683 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1">1</a></td>
683 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1">1</a></td>
684 <td class="code">
684 <td class="code">
685 <pre>diff --git a/file_ with update.txt b/file_changed _.txt
685 <pre>diff --git a/file_ with update.txt b/file_changed _.txt
686 </pre>
686 </pre>
687 </td>
687 </td>
688 </tr>
688 </tr>
689 <tr class="line add">
689 <tr class="line add">
690 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
690 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
691 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
691 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
692 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2">2</a></td>
692 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2">2</a></td>
693 <td class="code">
693 <td class="code">
694 <pre>rename from file_ with update.txt
694 <pre>rename from file_ with update.txt
695 </pre>
695 </pre>
696 </td>
696 </td>
697 </tr>
697 </tr>
698 <tr class="line add">
698 <tr class="line add">
699 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
699 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
700 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
700 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
701 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3">3</a></td>
701 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3">3</a></td>
702 <td class="code">
702 <td class="code">
703 <pre>rename to file_changed _.txt</pre>
703 <pre>rename to file_changed _.txt</pre>
704 </td>
704 </td>
705 </tr>
705 </tr>
706 </table>
706 </table>
707 </div>
707 </div>
708 </div>
708 </div>
709 </div>
709 </div>
710
710
711 </td>
711 </td>
712 </tr>
712 </tr>
713
713
714 </table>
714 </table>
715 </div>
715 </div>
716 </div>
716 </div>
717 </div>
717 </div>
718
718
719 </td>
719 </td>
720 </tr>
720 </tr>
721 </table>
721 </table>
722 </div>
722 </div>
723 </div>
723 </div>
724 </div>
724 </div>
725
725
726
726
727
727
728
728
729 <div id="comment-inline-form-template" style="display: none;">
729 <div id="comment-inline-form-template" style="display: none;">
730 <div class="comment-inline-form ac">
730 <div class="comment-inline-form ac">
731 <div class="overlay"><div class="overlay-text">Submitting...</div></div>
731 <div class="overlay"><div class="overlay-text">Submitting...</div></div>
732 <form action="#" class="inline-form" method="get">
732 <form action="#" class="inline-form" method="get">
733 <div id="edit-container_{1}" class="clearfix">
733 <div id="edit-container_{1}" class="clearfix">
734 <div class="comment-title pull-left">
734 <div class="comment-title pull-left">
735 Commenting on line {1}.
735 Commenting on line {1}.
736 </div>
736 </div>
737 <div class="comment-help pull-right">
737 <div class="comment-help pull-right">
738 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
738 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
739 </div>
739 </div>
740 <div style="clear: both"></div>
740 <div style="clear: both"></div>
741 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
741 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
742 </div>
742 </div>
743 <div id="preview-container_{1}" class="clearfix" style="display: none;">
743 <div id="preview-container_{1}" class="clearfix" style="display: none;">
744 <div class="comment-help">
744 <div class="comment-help">
745 Comment preview
745 Comment preview
746 </div>
746 </div>
747 <div id="preview-box_{1}" class="preview-box"></div>
747 <div id="preview-box_{1}" class="preview-box"></div>
748 </div>
748 </div>
749 <div class="comment-button pull-right">
749 <div class="comment-button pull-right">
750 <input type="hidden" name="f_path" value="{0}">
750 <input type="hidden" name="f_path" value="{0}">
751 <input type="hidden" name="line" value="{1}">
751 <input type="hidden" name="line" value="{1}">
752 <div id="preview-btn_{1}" class="btn btn-default">Preview</div>
752 <div id="preview-btn_{1}" class="btn btn-default">Preview</div>
753 <div id="edit-btn_{1}" class="btn" style="display: none;">Edit</div>
753 <div id="edit-btn_{1}" class="btn" style="display: none;">Edit</div>
754 <input class="btn btn-success save-inline-form" id="save" name="save" type="submit" value="Comment" />
754 <input class="btn btn-success save-inline-form" id="save" name="save" type="submit" value="Comment" />
755 </div>
755 </div>
756 <div class="comment-button hide-inline-form-button">
756 <div class="comment-button hide-inline-form-button">
757 <input class="btn hide-inline-form" id="hide-inline-form" name="hide-inline-form" type="reset" value="Cancel" />
757 <input class="btn hide-inline-form" id="hide-inline-form" name="hide-inline-form" type="reset" value="Cancel" />
758 </div>
758 </div>
759 </form>
759 </form>
760 </div>
760 </div>
761 </div>
761 </div>
762
762
763
763
764
764
765 <div class="comments">
765 <div class="comments">
766 <div id="inline-comments-container">
766 <div id="inline-comments-container">
767
767
768 <h2>0 Pull Request Comments</h2>
768 <h2>0 Pull Request Comments</h2>
769
769
770
770
771 </div>
771 </div>
772
772
773 </div>
773 </div>
774
774
775
775
776
776
777
777
778 <div class="pull-request-merge">
778 <div class="pull-request-merge">
779 </div>
779 </div>
780 <div class="comments">
780 <div class="comments">
781 <div class="comment-form ac">
781 <div class="comment-form ac">
782 <form action="/rhodecode-momentum/pull-request-comment/720" id="comments_form" method="POST">
782 <form action="/rhodecode-momentum/pull-request-comment/720" id="comments_form" method="POST">
783 <div style="display: none;"><input id="csrf_token" name="csrf_token" type="hidden" value="6dbc0b19ac65237df65d57202a3e1f2df4153e38" /></div>
783 <div style="display: none;"><input id="csrf_token" name="csrf_token" type="hidden" value="6dbc0b19ac65237df65d57202a3e1f2df4153e38" /></div>
784 <div id="edit-container" class="clearfix">
784 <div id="edit-container" class="clearfix">
785 <div class="comment-title pull-left">
785 <div class="comment-title pull-left">
786 Create a comment on this Pull Request.
786 Create a comment on this Pull Request.
787 </div>
787 </div>
788 <div class="comment-help pull-right">
788 <div class="comment-help pull-right">
789 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
789 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
790 </div>
790 </div>
791 <div style="clear: both"></div>
791 <div style="clear: both"></div>
792 <textarea class="comment-block-ta" id="text" name="text"></textarea>
792 <textarea class="comment-block-ta" id="text" name="text"></textarea>
793 </div>
793 </div>
794
794
795 <div id="preview-container" class="clearfix" style="display: none;">
795 <div id="preview-container" class="clearfix" style="display: none;">
796 <div class="comment-title">
796 <div class="comment-title">
797 Comment preview
797 Comment preview
798 </div>
798 </div>
799 <div id="preview-box" class="preview-box"></div>
799 <div id="preview-box" class="preview-box"></div>
800 </div>
800 </div>
801
801
802 <div id="comment_form_extras">
802 <div id="comment_form_extras">
803 </div>
803 </div>
804 <div class="action-button pull-right">
804 <div class="action-button pull-right">
805 <div id="preview-btn" class="btn">
805 <div id="preview-btn" class="btn">
806 Preview
806 Preview
807 </div>
807 </div>
808 <div id="edit-btn" class="btn" style="display: none;">
808 <div id="edit-btn" class="btn" style="display: none;">
809 Edit
809 Edit
810 </div>
810 </div>
811 <div class="comment-button">
811 <div class="comment-button">
812 <input class="btn btn-small btn-success comment-button-input" id="save" name="save" type="submit" value="Comment" />
812 <input class="btn btn-small btn-success comment-button-input" id="save" name="save" type="submit" value="Comment" />
813 </div>
813 </div>
814 </div>
814 </div>
815 </form>
815 </form>
816 </div>
816 </div>
817 </div>
817 </div>
818 <script>
818 <script>
819
819
820 $(document).ready(function() {
820 $(document).ready(function() {
821
821
822 var cm = initCommentBoxCodeMirror('#text');
822 var cm = initCommentBoxCodeMirror('#text');
823
823
824 // main form preview
824 // main form preview
825 $('#preview-btn').on('click', function(e) {
825 $('#preview-btn').on('click', function(e) {
826 $('#preview-btn').hide();
826 $('#preview-btn').hide();
827 $('#edit-btn').show();
827 $('#edit-btn').show();
828 var _text = cm.getValue();
828 var _text = cm.getValue();
829 if (!_text) {
829 if (!_text) {
830 return;
830 return;
831 }
831 }
832 var post_data = {
832 var post_data = {
833 'text': _text,
833 'text': _text,
834 'renderer': DEFAULT_RENDERER,
834 'renderer': DEFAULT_RENDERER,
835 'csrf_token': CSRF_TOKEN
835 'csrf_token': CSRF_TOKEN
836 };
836 };
837 var previewbox = $('#preview-box');
837 var previewbox = $('#preview-box');
838 previewbox.addClass('unloaded');
838 previewbox.addClass('unloaded');
839 previewbox.html(_gettext('Loading ...'));
839 previewbox.html(_gettext('Loading ...'));
840 $('#edit-container').hide();
840 $('#edit-container').hide();
841 $('#preview-container').show();
841 $('#preview-container').show();
842
842
843 var url = pyroutes.url('changeset_comment_preview', {'repo_name': 'rhodecode-momentum'});
843 var url = pyroutes.url('changeset_comment_preview', {'repo_name': 'rhodecode-momentum'});
844
844
845 ajaxPOST(url, post_data, function(o) {
845 ajaxPOST(url, post_data, function(o) {
846 previewbox.html(o);
846 previewbox.html(o);
847 previewbox.removeClass('unloaded');
847 previewbox.removeClass('unloaded');
848 });
848 });
849 });
849 });
850 $('#edit-btn').on('click', function(e) {
850 $('#edit-btn').on('click', function(e) {
851 $('#preview-btn').show();
851 $('#preview-btn').show();
852 $('#edit-btn').hide();
852 $('#edit-btn').hide();
853 $('#edit-container').show();
853 $('#edit-container').show();
854 $('#preview-container').hide();
854 $('#preview-container').hide();
855 });
855 });
856
856
857 var formatChangeStatus = function(state, escapeMarkup) {
857 var formatChangeStatus = function(state, escapeMarkup) {
858 var originalOption = state.element;
858 var originalOption = state.element;
859 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
859 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
860 '<span>' + escapeMarkup(state.text) + '</span>';
860 '<span>' + escapeMarkup(state.text) + '</span>';
861 };
861 };
862
862
863 var formatResult = function(result, container, query, escapeMarkup) {
863 var formatResult = function(result, container, query, escapeMarkup) {
864 return formatChangeStatus(result, escapeMarkup);
864 return formatChangeStatus(result, escapeMarkup);
865 };
865 };
866
866
867 var formatSelection = function(data, container, escapeMarkup) {
867 var formatSelection = function(data, container, escapeMarkup) {
868 return formatChangeStatus(data, escapeMarkup);
868 return formatChangeStatus(data, escapeMarkup);
869 };
869 };
870
870
871 $('#change_status_general').select2({
871 $('#change_status_general').select2({
872 placeholder: "Status Review",
872 placeholder: "Status Review",
873 formatResult: formatResult,
873 formatResult: formatResult,
874 formatSelection: formatSelection,
874 formatSelection: formatSelection,
875 containerCssClass: "drop-menu status_box_menu",
875 containerCssClass: "drop-menu status_box_menu",
876 dropdownCssClass: "drop-menu-dropdown",
876 dropdownCssClass: "drop-menu-dropdown",
877 dropdownAutoWidth: true,
877 dropdownAutoWidth: true,
878 minimumResultsForSearch: -1
878 minimumResultsForSearch: -1
879 });
879 });
880 });
880 });
881 </script>
881 </script>
882
882
883
883
884 <script type="text/javascript">
884 <script type="text/javascript">
885 // TODO: switch this to pyroutes
885 // TODO: switch this to pyroutes
886 AJAX_COMMENT_DELETE_URL = "/rhodecode-momentum/pull-request-comment/__COMMENT_ID__/delete";
886 AJAX_COMMENT_DELETE_URL = "/rhodecode-momentum/pull-request-comment/__COMMENT_ID__/delete";
887
887
888 $(function(){
888 $(function(){
889 ReviewerAutoComplete('#user');
889 ReviewerAutoComplete('#user');
890
890
891 $('#open_edit_reviewers').on('click', function(e){
891 $('#open_edit_reviewers').on('click', function(e){
892 $('#open_edit_reviewers').hide();
892 $('#open_edit_reviewers').hide();
893 $('#close_edit_reviewers').show();
893 $('#close_edit_reviewers').show();
894 $('#add_reviewer_input').show();
894 $('#add_reviewer_input').show();
895 $('.reviewer_member_remove').css('visibility', 'visible');
895 $('.reviewer_member_remove').css('visibility', 'visible');
896 });
896 });
897
897
898 $('#close_edit_reviewers').on('click', function(e){
898 $('#close_edit_reviewers').on('click', function(e){
899 $('#open_edit_reviewers').show();
899 $('#open_edit_reviewers').show();
900 $('#close_edit_reviewers').hide();
900 $('#close_edit_reviewers').hide();
901 $('#add_reviewer_input').hide();
901 $('#add_reviewer_input').hide();
902 $('.reviewer_member_remove').css('visibility', 'hidden');
902 $('.reviewer_member_remove').css('visibility', 'hidden');
903 });
903 });
904
904
905 $('.show-inline-comments').on('change', function(e){
905 $('.show-inline-comments').on('change', function(e){
906 var show = 'none';
906 var show = 'none';
907 var target = e.currentTarget;
907 var target = e.currentTarget;
908 if(target.checked){
908 if(target.checked){
909 show = ''
909 show = ''
910 }
910 }
911 var boxid = $(target).attr('id_for');
911 var boxid = $(target).attr('id_for');
912 var comments = $('#{0} .inline-comments'.format(boxid));
912 var comments = $('#{0} .inline-comments'.format(boxid));
913 var fn_display = function(idx){
913 var fn_display = function(idx){
914 $(this).css('display', show);
914 $(this).css('display', show);
915 };
915 };
916 $(comments).each(fn_display);
916 $(comments).each(fn_display);
917 var btns = $('#{0} .inline-comments-button'.format(boxid));
917 var btns = $('#{0} .inline-comments-button'.format(boxid));
918 $(btns).each(fn_display);
918 $(btns).each(fn_display);
919 });
919 });
920
920
921 var commentTotals = {};
921 var commentTotals = {};
922 $.each(file_comments, function(i, comment) {
922 $.each(file_comments, function(i, comment) {
923 var path = $(comment).attr('path');
923 var path = $(comment).attr('path');
924 var comms = $(comment).children().length;
924 var comms = $(comment).children().length;
925 if (path in commentTotals) {
925 if (path in commentTotals) {
926 commentTotals[path] += comms;
926 commentTotals[path] += comms;
927 } else {
927 } else {
928 commentTotals[path] = comms;
928 commentTotals[path] = comms;
929 }
929 }
930 });
930 });
931 $.each(commentTotals, function(path, total) {
931 $.each(commentTotals, function(path, total) {
932 var elem = $('.comment-bubble[data-path="'+ path +'"]')
932 var elem = $('.comment-bubble[data-path="'+ path +'"]')
933 elem.css('visibility', 'visible');
933 elem.css('visibility', 'visible');
934 elem.html(elem.html() + ' ' + total );
934 elem.html(elem.html() + ' ' + total );
935 });
935 });
936
936
937 $('#merge_pull_request_form').submit(function() {
937 $('#merge_pull_request_form').submit(function() {
938 if (!$('#merge_pull_request').attr('disabled')) {
938 if (!$('#merge_pull_request').attr('disabled')) {
939 $('#merge_pull_request').attr('disabled', 'disabled');
939 $('#merge_pull_request').attr('disabled', 'disabled');
940 }
940 }
941 return true;
941 return true;
942 });
942 });
943
943
944 $('#update_pull_request').on('click', function(e){
944 $('#update_pull_request').on('click', function(e){
945 updateReviewers(undefined, "rhodecode-momentum", "720");
945 updateReviewers(undefined, "rhodecode-momentum", "720");
946 });
946 });
947
947
948 $('#update_commits').on('click', function(e){
948 $('#update_commits').on('click', function(e){
949 updateCommits("rhodecode-momentum", "720");
949 updateCommits("rhodecode-momentum", "720");
950 });
950 });
951
951
952 $('#close_pull_request').on('click', function(e){
953 closePullRequest("rhodecode-momentum", "720");
954 });
955 })
952 })
956 </script>
953 </script>
957
954
958 </div>
955 </div>
959 </div></div>
956 </div></div>
960
957
961 </div>
958 </div>
962
959
963
960
964 </%def>
961 </%def>
@@ -1,865 +1,860 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 %if c.rhodecode_name:
6 %if c.rhodecode_name:
7 &middot; ${h.branding(c.rhodecode_name)}
7 &middot; ${h.branding(c.rhodecode_name)}
8 %endif
8 %endif
9 </%def>
9 </%def>
10
10
11 <%def name="breadcrumbs_links()">
11 <%def name="breadcrumbs_links()">
12 <span id="pr-title">
12 <span id="pr-title">
13 ${c.pull_request.title}
13 ${c.pull_request.title}
14 %if c.pull_request.is_closed():
14 %if c.pull_request.is_closed():
15 (${_('Closed')})
15 (${_('Closed')})
16 %endif
16 %endif
17 </span>
17 </span>
18 <div id="pr-title-edit" class="input" style="display: none;">
18 <div id="pr-title-edit" class="input" style="display: none;">
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 </div>
20 </div>
21 </%def>
21 </%def>
22
22
23 <%def name="menu_bar_nav()">
23 <%def name="menu_bar_nav()">
24 ${self.menu_items(active='repositories')}
24 ${self.menu_items(active='repositories')}
25 </%def>
25 </%def>
26
26
27 <%def name="menu_bar_subnav()">
27 <%def name="menu_bar_subnav()">
28 ${self.repo_menu(active='showpullrequest')}
28 ${self.repo_menu(active='showpullrequest')}
29 </%def>
29 </%def>
30
30
31 <%def name="main()">
31 <%def name="main()">
32
32
33 <script type="text/javascript">
33 <script type="text/javascript">
34 // TODO: marcink switch this to pyroutes
34 // TODO: marcink switch this to pyroutes
35 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 </script>
37 </script>
38 <div class="box">
38 <div class="box">
39
39
40 <div class="title">
40 <div class="title">
41 ${self.repo_page_title(c.rhodecode_db_repo)}
41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 </div>
42 </div>
43
43
44 ${self.breadcrumbs()}
44 ${self.breadcrumbs()}
45
45
46 <div class="box pr-summary">
46 <div class="box pr-summary">
47
47
48 <div class="summary-details block-left">
48 <div class="summary-details block-left">
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 <div class="pr-details-title">
50 <div class="pr-details-title">
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 %if c.allowed_to_update:
52 %if c.allowed_to_update:
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 % if c.allowed_to_delete:
54 % if c.allowed_to_delete:
55 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
55 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 ${h.end_form()}
58 ${h.end_form()}
59 % else:
59 % else:
60 ${_('Delete')}
60 ${_('Delete')}
61 % endif
61 % endif
62 </div>
62 </div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 %endif
65 %endif
66 </div>
66 </div>
67
67
68 <div id="summary" class="fields pr-details-content">
68 <div id="summary" class="fields pr-details-content">
69 <div class="field">
69 <div class="field">
70 <div class="label-summary">
70 <div class="label-summary">
71 <label>${_('Source')}:</label>
71 <label>${_('Source')}:</label>
72 </div>
72 </div>
73 <div class="input">
73 <div class="input">
74 <div class="pr-origininfo">
74 <div class="pr-origininfo">
75 ## branch link is only valid if it is a branch
75 ## branch link is only valid if it is a branch
76 <span class="tag">
76 <span class="tag">
77 %if c.pull_request.source_ref_parts.type == 'branch':
77 %if c.pull_request.source_ref_parts.type == 'branch':
78 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
78 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 %else:
79 %else:
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 %endif
81 %endif
82 </span>
82 </span>
83 <span class="clone-url">
83 <span class="clone-url">
84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 </span>
85 </span>
86 <br/>
86 <br/>
87 % if c.ancestor_commit:
87 % if c.ancestor_commit:
88 ${_('Common ancestor')}:
88 ${_('Common ancestor')}:
89 <code><a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
89 <code><a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 % endif
90 % endif
91 </div>
91 </div>
92 <div class="pr-pullinfo">
92 <div class="pr-pullinfo">
93 %if h.is_hg(c.pull_request.source_repo):
93 %if h.is_hg(c.pull_request.source_repo):
94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
95 %elif h.is_git(c.pull_request.source_repo):
95 %elif h.is_git(c.pull_request.source_repo):
96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
97 %endif
97 %endif
98 </div>
98 </div>
99 </div>
99 </div>
100 </div>
100 </div>
101 <div class="field">
101 <div class="field">
102 <div class="label-summary">
102 <div class="label-summary">
103 <label>${_('Target')}:</label>
103 <label>${_('Target')}:</label>
104 </div>
104 </div>
105 <div class="input">
105 <div class="input">
106 <div class="pr-targetinfo">
106 <div class="pr-targetinfo">
107 ## branch link is only valid if it is a branch
107 ## branch link is only valid if it is a branch
108 <span class="tag">
108 <span class="tag">
109 %if c.pull_request.target_ref_parts.type == 'branch':
109 %if c.pull_request.target_ref_parts.type == 'branch':
110 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
110 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
111 %else:
111 %else:
112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
113 %endif
113 %endif
114 </span>
114 </span>
115 <span class="clone-url">
115 <span class="clone-url">
116 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
116 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
117 </span>
117 </span>
118 </div>
118 </div>
119 </div>
119 </div>
120 </div>
120 </div>
121
121
122 ## Link to the shadow repository.
122 ## Link to the shadow repository.
123 <div class="field">
123 <div class="field">
124 <div class="label-summary">
124 <div class="label-summary">
125 <label>${_('Merge')}:</label>
125 <label>${_('Merge')}:</label>
126 </div>
126 </div>
127 <div class="input">
127 <div class="input">
128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
129 <div class="pr-mergeinfo">
129 <div class="pr-mergeinfo">
130 %if h.is_hg(c.pull_request.target_repo):
130 %if h.is_hg(c.pull_request.target_repo):
131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
132 %elif h.is_git(c.pull_request.target_repo):
132 %elif h.is_git(c.pull_request.target_repo):
133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
134 %endif
134 %endif
135 </div>
135 </div>
136 % else:
136 % else:
137 <div class="">
137 <div class="">
138 ${_('Shadow repository data not available')}.
138 ${_('Shadow repository data not available')}.
139 </div>
139 </div>
140 % endif
140 % endif
141 </div>
141 </div>
142 </div>
142 </div>
143
143
144 <div class="field">
144 <div class="field">
145 <div class="label-summary">
145 <div class="label-summary">
146 <label>${_('Review')}:</label>
146 <label>${_('Review')}:</label>
147 </div>
147 </div>
148 <div class="input">
148 <div class="input">
149 %if c.pull_request_review_status:
149 %if c.pull_request_review_status:
150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
151 <span class="changeset-status-lbl tooltip">
151 <span class="changeset-status-lbl tooltip">
152 %if c.pull_request.is_closed():
152 %if c.pull_request.is_closed():
153 ${_('Closed')},
153 ${_('Closed')},
154 %endif
154 %endif
155 ${h.commit_status_lbl(c.pull_request_review_status)}
155 ${h.commit_status_lbl(c.pull_request_review_status)}
156 </span>
156 </span>
157 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
157 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
158 %endif
158 %endif
159 </div>
159 </div>
160 </div>
160 </div>
161 <div class="field">
161 <div class="field">
162 <div class="pr-description-label label-summary">
162 <div class="pr-description-label label-summary">
163 <label>${_('Description')}:</label>
163 <label>${_('Description')}:</label>
164 </div>
164 </div>
165 <div id="pr-desc" class="input">
165 <div id="pr-desc" class="input">
166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
167 </div>
167 </div>
168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
170 </div>
170 </div>
171 </div>
171 </div>
172
172
173 <div class="field">
173 <div class="field">
174 <div class="label-summary">
174 <div class="label-summary">
175 <label>${_('Versions')}:</label>
175 <label>${_('Versions')}:</label>
176 </div>
176 </div>
177
177
178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
180
180
181 <div class="pr-versions">
181 <div class="pr-versions">
182 % if c.show_version_changes:
182 % if c.show_version_changes:
183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
186 data-toggle-on="${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
186 data-toggle-on="${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
187 data-toggle-off="${_('Hide all versions of this pull request')}">
187 data-toggle-off="${_('Hide all versions of this pull request')}">
188 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
188 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
189 </a>
189 </a>
190 <table>
190 <table>
191 ## SHOW ALL VERSIONS OF PR
191 ## SHOW ALL VERSIONS OF PR
192 <% ver_pr = None %>
192 <% ver_pr = None %>
193
193
194 % for data in reversed(list(enumerate(c.versions, 1))):
194 % for data in reversed(list(enumerate(c.versions, 1))):
195 <% ver_pos = data[0] %>
195 <% ver_pos = data[0] %>
196 <% ver = data[1] %>
196 <% ver = data[1] %>
197 <% ver_pr = ver.pull_request_version_id %>
197 <% ver_pr = ver.pull_request_version_id %>
198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
199
199
200 <tr class="version-pr" style="display: ${display_row}">
200 <tr class="version-pr" style="display: ${display_row}">
201 <td>
201 <td>
202 <code>
202 <code>
203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
204 </code>
204 </code>
205 </td>
205 </td>
206 <td>
206 <td>
207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
209 </td>
209 </td>
210 <td>
210 <td>
211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
213 </div>
213 </div>
214 </td>
214 </td>
215 <td>
215 <td>
216 % if c.at_version_num != ver_pr:
216 % if c.at_version_num != ver_pr:
217 <i class="icon-comment"></i>
217 <i class="icon-comment"></i>
218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
220 </code>
220 </code>
221 % endif
221 % endif
222 </td>
222 </td>
223 <td>
223 <td>
224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
225 </td>
225 </td>
226 <td>
226 <td>
227 ${h.age_component(ver.updated_on, time_is_local=True)}
227 ${h.age_component(ver.updated_on, time_is_local=True)}
228 </td>
228 </td>
229 </tr>
229 </tr>
230 % endfor
230 % endfor
231
231
232 <tr>
232 <tr>
233 <td colspan="6">
233 <td colspan="6">
234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
235 data-label-text-locked="${_('select versions to show changes')}"
235 data-label-text-locked="${_('select versions to show changes')}"
236 data-label-text-diff="${_('show changes between versions')}"
236 data-label-text-diff="${_('show changes between versions')}"
237 data-label-text-show="${_('show pull request for this version')}"
237 data-label-text-show="${_('show pull request for this version')}"
238 >
238 >
239 ${_('select versions to show changes')}
239 ${_('select versions to show changes')}
240 </button>
240 </button>
241 </td>
241 </td>
242 </tr>
242 </tr>
243
243
244 ## show comment/inline comments summary
244 ## show comment/inline comments summary
245 <%def name="comments_summary()">
245 <%def name="comments_summary()">
246 <tr>
246 <tr>
247 <td colspan="6" class="comments-summary-td">
247 <td colspan="6" class="comments-summary-td">
248
248
249 % if c.at_version:
249 % if c.at_version:
250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
252 ${_('Comments at this version')}:
252 ${_('Comments at this version')}:
253 % else:
253 % else:
254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
256 ${_('Comments for this pull request')}:
256 ${_('Comments for this pull request')}:
257 % endif
257 % endif
258
258
259
259
260 %if general_comm_count_ver:
260 %if general_comm_count_ver:
261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
262 %else:
262 %else:
263 ${_("%d General ") % general_comm_count_ver}
263 ${_("%d General ") % general_comm_count_ver}
264 %endif
264 %endif
265
265
266 %if inline_comm_count_ver:
266 %if inline_comm_count_ver:
267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
268 %else:
268 %else:
269 , ${_("%d Inline") % inline_comm_count_ver}
269 , ${_("%d Inline") % inline_comm_count_ver}
270 %endif
270 %endif
271
271
272 %if outdated_comm_count_ver:
272 %if outdated_comm_count_ver:
273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
276 %else:
276 %else:
277 , ${_("%d Outdated") % outdated_comm_count_ver}
277 , ${_("%d Outdated") % outdated_comm_count_ver}
278 %endif
278 %endif
279 </td>
279 </td>
280 </tr>
280 </tr>
281 </%def>
281 </%def>
282 ${comments_summary()}
282 ${comments_summary()}
283 </table>
283 </table>
284 % else:
284 % else:
285 <div class="input">
285 <div class="input">
286 ${_('Pull request versions not available')}.
286 ${_('Pull request versions not available')}.
287 </div>
287 </div>
288 <div>
288 <div>
289 <table>
289 <table>
290 ${comments_summary()}
290 ${comments_summary()}
291 </table>
291 </table>
292 </div>
292 </div>
293 % endif
293 % endif
294 </div>
294 </div>
295 </div>
295 </div>
296
296
297 <div id="pr-save" class="field" style="display: none;">
297 <div id="pr-save" class="field" style="display: none;">
298 <div class="label-summary"></div>
298 <div class="label-summary"></div>
299 <div class="input">
299 <div class="input">
300 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
300 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
301 </div>
301 </div>
302 </div>
302 </div>
303 </div>
303 </div>
304 </div>
304 </div>
305 <div>
305 <div>
306 ## AUTHOR
306 ## AUTHOR
307 <div class="reviewers-title block-right">
307 <div class="reviewers-title block-right">
308 <div class="pr-details-title">
308 <div class="pr-details-title">
309 ${_('Author of this pull request')}
309 ${_('Author of this pull request')}
310 </div>
310 </div>
311 </div>
311 </div>
312 <div class="block-right pr-details-content reviewers">
312 <div class="block-right pr-details-content reviewers">
313 <ul class="group_members">
313 <ul class="group_members">
314 <li>
314 <li>
315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
316 </li>
316 </li>
317 </ul>
317 </ul>
318 </div>
318 </div>
319
319
320 ## REVIEW RULES
320 ## REVIEW RULES
321 <div id="review_rules" style="display: none" class="reviewers-title block-right">
321 <div id="review_rules" style="display: none" class="reviewers-title block-right">
322 <div class="pr-details-title">
322 <div class="pr-details-title">
323 ${_('Reviewer rules')}
323 ${_('Reviewer rules')}
324 %if c.allowed_to_update:
324 %if c.allowed_to_update:
325 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
325 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
326 %endif
326 %endif
327 </div>
327 </div>
328 <div class="pr-reviewer-rules">
328 <div class="pr-reviewer-rules">
329 ## review rules will be appended here, by default reviewers logic
329 ## review rules will be appended here, by default reviewers logic
330 </div>
330 </div>
331 <input id="review_data" type="hidden" name="review_data" value="">
331 <input id="review_data" type="hidden" name="review_data" value="">
332 </div>
332 </div>
333
333
334 ## REVIEWERS
334 ## REVIEWERS
335 <div class="reviewers-title block-right">
335 <div class="reviewers-title block-right">
336 <div class="pr-details-title">
336 <div class="pr-details-title">
337 ${_('Pull request reviewers')}
337 ${_('Pull request reviewers')}
338 %if c.allowed_to_update:
338 %if c.allowed_to_update:
339 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
339 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
340 %endif
340 %endif
341 </div>
341 </div>
342 </div>
342 </div>
343 <div id="reviewers" class="block-right pr-details-content reviewers">
343 <div id="reviewers" class="block-right pr-details-content reviewers">
344 ## members goes here !
344 ## members goes here !
345 <input type="hidden" name="__start__" value="review_members:sequence">
345 <input type="hidden" name="__start__" value="review_members:sequence">
346 <ul id="review_members" class="group_members">
346 <ul id="review_members" class="group_members">
347 %for member,reasons,mandatory,status in c.pull_request_reviewers:
347 %for member,reasons,mandatory,status in c.pull_request_reviewers:
348 <li id="reviewer_${member.user_id}" class="reviewer_entry">
348 <li id="reviewer_${member.user_id}" class="reviewer_entry">
349 <div class="reviewers_member">
349 <div class="reviewers_member">
350 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
350 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
351 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
351 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
352 </div>
352 </div>
353 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
353 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
354 ${self.gravatar_with_user(member.email, 16)}
354 ${self.gravatar_with_user(member.email, 16)}
355 </div>
355 </div>
356 <input type="hidden" name="__start__" value="reviewer:mapping">
356 <input type="hidden" name="__start__" value="reviewer:mapping">
357 <input type="hidden" name="__start__" value="reasons:sequence">
357 <input type="hidden" name="__start__" value="reasons:sequence">
358 %for reason in reasons:
358 %for reason in reasons:
359 <div class="reviewer_reason">- ${reason}</div>
359 <div class="reviewer_reason">- ${reason}</div>
360 <input type="hidden" name="reason" value="${reason}">
360 <input type="hidden" name="reason" value="${reason}">
361
361
362 %endfor
362 %endfor
363 <input type="hidden" name="__end__" value="reasons:sequence">
363 <input type="hidden" name="__end__" value="reasons:sequence">
364 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
364 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
365 <input type="hidden" name="mandatory" value="${mandatory}"/>
365 <input type="hidden" name="mandatory" value="${mandatory}"/>
366 <input type="hidden" name="__end__" value="reviewer:mapping">
366 <input type="hidden" name="__end__" value="reviewer:mapping">
367 % if mandatory:
367 % if mandatory:
368 <div class="reviewer_member_mandatory_remove">
368 <div class="reviewer_member_mandatory_remove">
369 <i class="icon-remove-sign"></i>
369 <i class="icon-remove-sign"></i>
370 </div>
370 </div>
371 <div class="reviewer_member_mandatory">
371 <div class="reviewer_member_mandatory">
372 <i class="icon-lock" title="Mandatory reviewer"></i>
372 <i class="icon-lock" title="Mandatory reviewer"></i>
373 </div>
373 </div>
374 % else:
374 % else:
375 %if c.allowed_to_update:
375 %if c.allowed_to_update:
376 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
376 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
377 <i class="icon-remove-sign" ></i>
377 <i class="icon-remove-sign" ></i>
378 </div>
378 </div>
379 %endif
379 %endif
380 % endif
380 % endif
381 </div>
381 </div>
382 </li>
382 </li>
383 %endfor
383 %endfor
384 </ul>
384 </ul>
385 <input type="hidden" name="__end__" value="review_members:sequence">
385 <input type="hidden" name="__end__" value="review_members:sequence">
386
386
387 %if not c.pull_request.is_closed():
387 %if not c.pull_request.is_closed():
388 <div id="add_reviewer" class="ac" style="display: none;">
388 <div id="add_reviewer" class="ac" style="display: none;">
389 %if c.allowed_to_update:
389 %if c.allowed_to_update:
390 % if not c.forbid_adding_reviewers:
390 % if not c.forbid_adding_reviewers:
391 <div id="add_reviewer_input" class="reviewer_ac">
391 <div id="add_reviewer_input" class="reviewer_ac">
392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
393 <div id="reviewers_container"></div>
393 <div id="reviewers_container"></div>
394 </div>
394 </div>
395 % endif
395 % endif
396 <div class="pull-right">
396 <div class="pull-right">
397 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
397 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
398 </div>
398 </div>
399 %endif
399 %endif
400 </div>
400 </div>
401 %endif
401 %endif
402 </div>
402 </div>
403 </div>
403 </div>
404 </div>
404 </div>
405 <div class="box">
405 <div class="box">
406 ##DIFF
406 ##DIFF
407 <div class="table" >
407 <div class="table" >
408 <div id="changeset_compare_view_content">
408 <div id="changeset_compare_view_content">
409 ##CS
409 ##CS
410 % if c.missing_requirements:
410 % if c.missing_requirements:
411 <div class="box">
411 <div class="box">
412 <div class="alert alert-warning">
412 <div class="alert alert-warning">
413 <div>
413 <div>
414 <strong>${_('Missing requirements:')}</strong>
414 <strong>${_('Missing requirements:')}</strong>
415 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
415 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
416 </div>
416 </div>
417 </div>
417 </div>
418 </div>
418 </div>
419 % elif c.missing_commits:
419 % elif c.missing_commits:
420 <div class="box">
420 <div class="box">
421 <div class="alert alert-warning">
421 <div class="alert alert-warning">
422 <div>
422 <div>
423 <strong>${_('Missing commits')}:</strong>
423 <strong>${_('Missing commits')}:</strong>
424 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
424 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
425 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
425 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
426 </div>
426 </div>
427 </div>
427 </div>
428 </div>
428 </div>
429 % endif
429 % endif
430
430
431 <div class="compare_view_commits_title">
431 <div class="compare_view_commits_title">
432 % if not c.compare_mode:
432 % if not c.compare_mode:
433
433
434 % if c.at_version_pos:
434 % if c.at_version_pos:
435 <h4>
435 <h4>
436 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
436 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
437 </h4>
437 </h4>
438 % endif
438 % endif
439
439
440 <div class="pull-left">
440 <div class="pull-left">
441 <div class="btn-group">
441 <div class="btn-group">
442 <a
442 <a
443 class="btn"
443 class="btn"
444 href="#"
444 href="#"
445 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
445 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
446 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
446 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
447 </a>
447 </a>
448 <a
448 <a
449 class="btn"
449 class="btn"
450 href="#"
450 href="#"
451 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
451 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
452 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
452 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
453 </a>
453 </a>
454 </div>
454 </div>
455 </div>
455 </div>
456
456
457 <div class="pull-right">
457 <div class="pull-right">
458 % if c.allowed_to_update and not c.pull_request.is_closed():
458 % if c.allowed_to_update and not c.pull_request.is_closed():
459 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
459 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
460 % else:
460 % else:
461 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
461 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
462 % endif
462 % endif
463
463
464 </div>
464 </div>
465 % endif
465 % endif
466 </div>
466 </div>
467
467
468 % if not c.missing_commits:
468 % if not c.missing_commits:
469 % if c.compare_mode:
469 % if c.compare_mode:
470 % if c.at_version:
470 % if c.at_version:
471 <h4>
471 <h4>
472 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
472 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
473 </h4>
473 </h4>
474
474
475 <div class="subtitle-compare">
475 <div class="subtitle-compare">
476 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
476 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
477 </div>
477 </div>
478
478
479 <div class="container">
479 <div class="container">
480 <table class="rctable compare_view_commits">
480 <table class="rctable compare_view_commits">
481 <tr>
481 <tr>
482 <th></th>
482 <th></th>
483 <th>${_('Time')}</th>
483 <th>${_('Time')}</th>
484 <th>${_('Author')}</th>
484 <th>${_('Author')}</th>
485 <th>${_('Commit')}</th>
485 <th>${_('Commit')}</th>
486 <th></th>
486 <th></th>
487 <th>${_('Description')}</th>
487 <th>${_('Description')}</th>
488 </tr>
488 </tr>
489
489
490 % for c_type, commit in c.commit_changes:
490 % for c_type, commit in c.commit_changes:
491 % if c_type in ['a', 'r']:
491 % if c_type in ['a', 'r']:
492 <%
492 <%
493 if c_type == 'a':
493 if c_type == 'a':
494 cc_title = _('Commit added in displayed changes')
494 cc_title = _('Commit added in displayed changes')
495 elif c_type == 'r':
495 elif c_type == 'r':
496 cc_title = _('Commit removed in displayed changes')
496 cc_title = _('Commit removed in displayed changes')
497 else:
497 else:
498 cc_title = ''
498 cc_title = ''
499 %>
499 %>
500 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
500 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
501 <td>
501 <td>
502 <div class="commit-change-indicator color-${c_type}-border">
502 <div class="commit-change-indicator color-${c_type}-border">
503 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
503 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
504 ${c_type.upper()}
504 ${c_type.upper()}
505 </div>
505 </div>
506 </div>
506 </div>
507 </td>
507 </td>
508 <td class="td-time">
508 <td class="td-time">
509 ${h.age_component(commit.date)}
509 ${h.age_component(commit.date)}
510 </td>
510 </td>
511 <td class="td-user">
511 <td class="td-user">
512 ${base.gravatar_with_user(commit.author, 16)}
512 ${base.gravatar_with_user(commit.author, 16)}
513 </td>
513 </td>
514 <td class="td-hash">
514 <td class="td-hash">
515 <code>
515 <code>
516 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
516 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
517 r${commit.revision}:${h.short_id(commit.raw_id)}
517 r${commit.revision}:${h.short_id(commit.raw_id)}
518 </a>
518 </a>
519 ${h.hidden('revisions', commit.raw_id)}
519 ${h.hidden('revisions', commit.raw_id)}
520 </code>
520 </code>
521 </td>
521 </td>
522 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
522 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
523 <div class="show_more_col">
523 <div class="show_more_col">
524 <i class="show_more"></i>
524 <i class="show_more"></i>
525 </div>
525 </div>
526 </td>
526 </td>
527 <td class="mid td-description">
527 <td class="mid td-description">
528 <div class="log-container truncate-wrap">
528 <div class="log-container truncate-wrap">
529 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
529 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
530 ${h.urlify_commit_message(commit.message, c.repo_name)}
530 ${h.urlify_commit_message(commit.message, c.repo_name)}
531 </div>
531 </div>
532 </div>
532 </div>
533 </td>
533 </td>
534 </tr>
534 </tr>
535 % endif
535 % endif
536 % endfor
536 % endfor
537 </table>
537 </table>
538 </div>
538 </div>
539
539
540 <script>
540 <script>
541 $('.expand_commit').on('click',function(e){
541 $('.expand_commit').on('click',function(e){
542 var target_expand = $(this);
542 var target_expand = $(this);
543 var cid = target_expand.data('commitId');
543 var cid = target_expand.data('commitId');
544
544
545 if (target_expand.hasClass('open')){
545 if (target_expand.hasClass('open')){
546 $('#c-'+cid).css({
546 $('#c-'+cid).css({
547 'height': '1.5em',
547 'height': '1.5em',
548 'white-space': 'nowrap',
548 'white-space': 'nowrap',
549 'text-overflow': 'ellipsis',
549 'text-overflow': 'ellipsis',
550 'overflow':'hidden'
550 'overflow':'hidden'
551 });
551 });
552 target_expand.removeClass('open');
552 target_expand.removeClass('open');
553 }
553 }
554 else {
554 else {
555 $('#c-'+cid).css({
555 $('#c-'+cid).css({
556 'height': 'auto',
556 'height': 'auto',
557 'white-space': 'pre-line',
557 'white-space': 'pre-line',
558 'text-overflow': 'initial',
558 'text-overflow': 'initial',
559 'overflow':'visible'
559 'overflow':'visible'
560 });
560 });
561 target_expand.addClass('open');
561 target_expand.addClass('open');
562 }
562 }
563 });
563 });
564 </script>
564 </script>
565
565
566 % endif
566 % endif
567
567
568 % else:
568 % else:
569 <%include file="/compare/compare_commits.mako" />
569 <%include file="/compare/compare_commits.mako" />
570 % endif
570 % endif
571
571
572 <div class="cs_files">
572 <div class="cs_files">
573 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
573 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
574 ${cbdiffs.render_diffset_menu()}
574 ${cbdiffs.render_diffset_menu()}
575 ${cbdiffs.render_diffset(
575 ${cbdiffs.render_diffset(
576 c.diffset, use_comments=True,
576 c.diffset, use_comments=True,
577 collapse_when_files_over=30,
577 collapse_when_files_over=30,
578 disable_new_comments=not c.allowed_to_comment,
578 disable_new_comments=not c.allowed_to_comment,
579 deleted_files_comments=c.deleted_files_comments)}
579 deleted_files_comments=c.deleted_files_comments)}
580 </div>
580 </div>
581 % else:
581 % else:
582 ## skipping commits we need to clear the view for missing commits
582 ## skipping commits we need to clear the view for missing commits
583 <div style="clear:both;"></div>
583 <div style="clear:both;"></div>
584 % endif
584 % endif
585
585
586 </div>
586 </div>
587 </div>
587 </div>
588
588
589 ## template for inline comment form
589 ## template for inline comment form
590 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
590 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
591
591
592 ## render general comments
592 ## render general comments
593
593
594 <div id="comment-tr-show">
594 <div id="comment-tr-show">
595 <div class="comment">
595 <div class="comment">
596 % if general_outdated_comm_count_ver:
596 % if general_outdated_comm_count_ver:
597 <div class="meta">
597 <div class="meta">
598 % if general_outdated_comm_count_ver == 1:
598 % if general_outdated_comm_count_ver == 1:
599 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
599 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
600 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
600 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
601 % else:
601 % else:
602 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
602 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
603 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
603 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
604 % endif
604 % endif
605 </div>
605 </div>
606 % endif
606 % endif
607 </div>
607 </div>
608 </div>
608 </div>
609
609
610 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
610 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
611
611
612 % if not c.pull_request.is_closed():
612 % if not c.pull_request.is_closed():
613 ## merge status, and merge action
613 ## merge status, and merge action
614 <div class="pull-request-merge">
614 <div class="pull-request-merge">
615 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
615 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
616 </div>
616 </div>
617
617
618 ## main comment form and it status
618 ## main comment form and it status
619 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
619 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
620 pull_request_id=c.pull_request.pull_request_id),
620 pull_request_id=c.pull_request.pull_request_id),
621 c.pull_request_review_status,
621 c.pull_request_review_status,
622 is_pull_request=True, change_status=c.allowed_to_change_status)}
622 is_pull_request=True, change_status=c.allowed_to_change_status)}
623 %endif
623 %endif
624
624
625 <script type="text/javascript">
625 <script type="text/javascript">
626 if (location.hash) {
626 if (location.hash) {
627 var result = splitDelimitedHash(location.hash);
627 var result = splitDelimitedHash(location.hash);
628 var line = $('html').find(result.loc);
628 var line = $('html').find(result.loc);
629 // show hidden comments if we use location.hash
629 // show hidden comments if we use location.hash
630 if (line.hasClass('comment-general')) {
630 if (line.hasClass('comment-general')) {
631 $(line).show();
631 $(line).show();
632 } else if (line.hasClass('comment-inline')) {
632 } else if (line.hasClass('comment-inline')) {
633 $(line).show();
633 $(line).show();
634 var $cb = $(line).closest('.cb');
634 var $cb = $(line).closest('.cb');
635 $cb.removeClass('cb-collapsed')
635 $cb.removeClass('cb-collapsed')
636 }
636 }
637 if (line.length > 0){
637 if (line.length > 0){
638 offsetScroll(line, 70);
638 offsetScroll(line, 70);
639 }
639 }
640 }
640 }
641
641
642 versionController = new VersionController();
642 versionController = new VersionController();
643 versionController.init();
643 versionController.init();
644
644
645 reviewersController = new ReviewersController();
645 reviewersController = new ReviewersController();
646
646
647 $(function(){
647 $(function(){
648
648
649 // custom code mirror
649 // custom code mirror
650 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
650 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
651
651
652 var PRDetails = {
652 var PRDetails = {
653 editButton: $('#open_edit_pullrequest'),
653 editButton: $('#open_edit_pullrequest'),
654 closeButton: $('#close_edit_pullrequest'),
654 closeButton: $('#close_edit_pullrequest'),
655 deleteButton: $('#delete_pullrequest'),
655 deleteButton: $('#delete_pullrequest'),
656 viewFields: $('#pr-desc, #pr-title'),
656 viewFields: $('#pr-desc, #pr-title'),
657 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
657 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
658
658
659 init: function() {
659 init: function() {
660 var that = this;
660 var that = this;
661 this.editButton.on('click', function(e) { that.edit(); });
661 this.editButton.on('click', function(e) { that.edit(); });
662 this.closeButton.on('click', function(e) { that.view(); });
662 this.closeButton.on('click', function(e) { that.view(); });
663 },
663 },
664
664
665 edit: function(event) {
665 edit: function(event) {
666 this.viewFields.hide();
666 this.viewFields.hide();
667 this.editButton.hide();
667 this.editButton.hide();
668 this.deleteButton.hide();
668 this.deleteButton.hide();
669 this.closeButton.show();
669 this.closeButton.show();
670 this.editFields.show();
670 this.editFields.show();
671 codeMirrorInstance.refresh();
671 codeMirrorInstance.refresh();
672 },
672 },
673
673
674 view: function(event) {
674 view: function(event) {
675 this.editButton.show();
675 this.editButton.show();
676 this.deleteButton.show();
676 this.deleteButton.show();
677 this.editFields.hide();
677 this.editFields.hide();
678 this.closeButton.hide();
678 this.closeButton.hide();
679 this.viewFields.show();
679 this.viewFields.show();
680 }
680 }
681 };
681 };
682
682
683 var ReviewersPanel = {
683 var ReviewersPanel = {
684 editButton: $('#open_edit_reviewers'),
684 editButton: $('#open_edit_reviewers'),
685 closeButton: $('#close_edit_reviewers'),
685 closeButton: $('#close_edit_reviewers'),
686 addButton: $('#add_reviewer'),
686 addButton: $('#add_reviewer'),
687 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'),
687 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'),
688
688
689 init: function() {
689 init: function() {
690 var self = this;
690 var self = this;
691 this.editButton.on('click', function(e) { self.edit(); });
691 this.editButton.on('click', function(e) { self.edit(); });
692 this.closeButton.on('click', function(e) { self.close(); });
692 this.closeButton.on('click', function(e) { self.close(); });
693 },
693 },
694
694
695 edit: function(event) {
695 edit: function(event) {
696 this.editButton.hide();
696 this.editButton.hide();
697 this.closeButton.show();
697 this.closeButton.show();
698 this.addButton.show();
698 this.addButton.show();
699 this.removeButtons.css('visibility', 'visible');
699 this.removeButtons.css('visibility', 'visible');
700 // review rules
700 // review rules
701 reviewersController.loadReviewRules(
701 reviewersController.loadReviewRules(
702 ${c.pull_request.reviewer_data_json | n});
702 ${c.pull_request.reviewer_data_json | n});
703 },
703 },
704
704
705 close: function(event) {
705 close: function(event) {
706 this.editButton.show();
706 this.editButton.show();
707 this.closeButton.hide();
707 this.closeButton.hide();
708 this.addButton.hide();
708 this.addButton.hide();
709 this.removeButtons.css('visibility', 'hidden');
709 this.removeButtons.css('visibility', 'hidden');
710 // hide review rules
710 // hide review rules
711 reviewersController.hideReviewRules()
711 reviewersController.hideReviewRules()
712 }
712 }
713 };
713 };
714
714
715 PRDetails.init();
715 PRDetails.init();
716 ReviewersPanel.init();
716 ReviewersPanel.init();
717
717
718 showOutdated = function(self){
718 showOutdated = function(self){
719 $('.comment-inline.comment-outdated').show();
719 $('.comment-inline.comment-outdated').show();
720 $('.filediff-outdated').show();
720 $('.filediff-outdated').show();
721 $('.showOutdatedComments').hide();
721 $('.showOutdatedComments').hide();
722 $('.hideOutdatedComments').show();
722 $('.hideOutdatedComments').show();
723 };
723 };
724
724
725 hideOutdated = function(self){
725 hideOutdated = function(self){
726 $('.comment-inline.comment-outdated').hide();
726 $('.comment-inline.comment-outdated').hide();
727 $('.filediff-outdated').hide();
727 $('.filediff-outdated').hide();
728 $('.hideOutdatedComments').hide();
728 $('.hideOutdatedComments').hide();
729 $('.showOutdatedComments').show();
729 $('.showOutdatedComments').show();
730 };
730 };
731
731
732 refreshMergeChecks = function(){
732 refreshMergeChecks = function(){
733 var loadUrl = "${h.url.current(merge_checks=1)}";
733 var loadUrl = "${h.url.current(merge_checks=1)}";
734 $('.pull-request-merge').css('opacity', 0.3);
734 $('.pull-request-merge').css('opacity', 0.3);
735 $('.action-buttons-extra').css('opacity', 0.3);
735 $('.action-buttons-extra').css('opacity', 0.3);
736
736
737 $('.pull-request-merge').load(
737 $('.pull-request-merge').load(
738 loadUrl, function() {
738 loadUrl, function() {
739 $('.pull-request-merge').css('opacity', 1);
739 $('.pull-request-merge').css('opacity', 1);
740
740
741 $('.action-buttons-extra').css('opacity', 1);
741 $('.action-buttons-extra').css('opacity', 1);
742 injectCloseAction();
742 injectCloseAction();
743 }
743 }
744 );
744 );
745 };
745 };
746
746
747 injectCloseAction = function() {
747 injectCloseAction = function() {
748 var closeAction = $('#close-pull-request-action').html();
748 var closeAction = $('#close-pull-request-action').html();
749 var $actionButtons = $('.action-buttons-extra');
749 var $actionButtons = $('.action-buttons-extra');
750 // clear the action before
750 // clear the action before
751 $actionButtons.html("");
751 $actionButtons.html("");
752 $actionButtons.html(closeAction);
752 $actionButtons.html(closeAction);
753 };
753 };
754
754
755 closePullRequest = function (status) {
755 closePullRequest = function (status) {
756 // inject closing flag
756 // inject closing flag
757 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
757 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
758 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
758 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
759 $(generalCommentForm.submitForm).submit();
759 $(generalCommentForm.submitForm).submit();
760 };
760 };
761
761
762 $('#show-outdated-comments').on('click', function(e){
762 $('#show-outdated-comments').on('click', function(e){
763 var button = $(this);
763 var button = $(this);
764 var outdated = $('.comment-outdated');
764 var outdated = $('.comment-outdated');
765
765
766 if (button.html() === "(Show)") {
766 if (button.html() === "(Show)") {
767 button.html("(Hide)");
767 button.html("(Hide)");
768 outdated.show();
768 outdated.show();
769 } else {
769 } else {
770 button.html("(Show)");
770 button.html("(Show)");
771 outdated.hide();
771 outdated.hide();
772 }
772 }
773 });
773 });
774
774
775 $('.show-inline-comments').on('change', function(e){
775 $('.show-inline-comments').on('change', function(e){
776 var show = 'none';
776 var show = 'none';
777 var target = e.currentTarget;
777 var target = e.currentTarget;
778 if(target.checked){
778 if(target.checked){
779 show = ''
779 show = ''
780 }
780 }
781 var boxid = $(target).attr('id_for');
781 var boxid = $(target).attr('id_for');
782 var comments = $('#{0} .inline-comments'.format(boxid));
782 var comments = $('#{0} .inline-comments'.format(boxid));
783 var fn_display = function(idx){
783 var fn_display = function(idx){
784 $(this).css('display', show);
784 $(this).css('display', show);
785 };
785 };
786 $(comments).each(fn_display);
786 $(comments).each(fn_display);
787 var btns = $('#{0} .inline-comments-button'.format(boxid));
787 var btns = $('#{0} .inline-comments-button'.format(boxid));
788 $(btns).each(fn_display);
788 $(btns).each(fn_display);
789 });
789 });
790
790
791 $('#merge_pull_request_form').submit(function() {
791 $('#merge_pull_request_form').submit(function() {
792 if (!$('#merge_pull_request').attr('disabled')) {
792 if (!$('#merge_pull_request').attr('disabled')) {
793 $('#merge_pull_request').attr('disabled', 'disabled');
793 $('#merge_pull_request').attr('disabled', 'disabled');
794 }
794 }
795 return true;
795 return true;
796 });
796 });
797
797
798 $('#edit_pull_request').on('click', function(e){
798 $('#edit_pull_request').on('click', function(e){
799 var title = $('#pr-title-input').val();
799 var title = $('#pr-title-input').val();
800 var description = codeMirrorInstance.getValue();
800 var description = codeMirrorInstance.getValue();
801 editPullRequest(
801 editPullRequest(
802 "${c.repo_name}", "${c.pull_request.pull_request_id}",
802 "${c.repo_name}", "${c.pull_request.pull_request_id}",
803 title, description);
803 title, description);
804 });
804 });
805
805
806 $('#update_pull_request').on('click', function(e){
806 $('#update_pull_request').on('click', function(e){
807 $(this).attr('disabled', 'disabled');
807 $(this).attr('disabled', 'disabled');
808 $(this).addClass('disabled');
808 $(this).addClass('disabled');
809 $(this).html(_gettext('Saving...'));
809 $(this).html(_gettext('Saving...'));
810 reviewersController.updateReviewers(
810 reviewersController.updateReviewers(
811 "${c.repo_name}", "${c.pull_request.pull_request_id}");
811 "${c.repo_name}", "${c.pull_request.pull_request_id}");
812 });
812 });
813
813
814 $('#update_commits').on('click', function(e){
814 $('#update_commits').on('click', function(e){
815 var isDisabled = !$(e.currentTarget).attr('disabled');
815 var isDisabled = !$(e.currentTarget).attr('disabled');
816 $(e.currentTarget).attr('disabled', 'disabled');
816 $(e.currentTarget).attr('disabled', 'disabled');
817 $(e.currentTarget).addClass('disabled');
817 $(e.currentTarget).addClass('disabled');
818 $(e.currentTarget).removeClass('btn-primary');
818 $(e.currentTarget).removeClass('btn-primary');
819 $(e.currentTarget).text(_gettext('Updating...'));
819 $(e.currentTarget).text(_gettext('Updating...'));
820 if(isDisabled){
820 if(isDisabled){
821 updateCommits(
821 updateCommits(
822 "${c.repo_name}", "${c.pull_request.pull_request_id}");
822 "${c.repo_name}", "${c.pull_request.pull_request_id}");
823 }
823 }
824 });
824 });
825 // fixing issue with caches on firefox
825 // fixing issue with caches on firefox
826 $('#update_commits').removeAttr("disabled");
826 $('#update_commits').removeAttr("disabled");
827
827
828 $('#close_pull_request').on('click', function(e){
829 closePullRequest(
830 "${c.repo_name}", "${c.pull_request.pull_request_id}");
831 });
832
833 $('.show-inline-comments').on('click', function(e){
828 $('.show-inline-comments').on('click', function(e){
834 var boxid = $(this).attr('data-comment-id');
829 var boxid = $(this).attr('data-comment-id');
835 var button = $(this);
830 var button = $(this);
836
831
837 if(button.hasClass("comments-visible")) {
832 if(button.hasClass("comments-visible")) {
838 $('#{0} .inline-comments'.format(boxid)).each(function(index){
833 $('#{0} .inline-comments'.format(boxid)).each(function(index){
839 $(this).hide();
834 $(this).hide();
840 });
835 });
841 button.removeClass("comments-visible");
836 button.removeClass("comments-visible");
842 } else {
837 } else {
843 $('#{0} .inline-comments'.format(boxid)).each(function(index){
838 $('#{0} .inline-comments'.format(boxid)).each(function(index){
844 $(this).show();
839 $(this).show();
845 });
840 });
846 button.addClass("comments-visible");
841 button.addClass("comments-visible");
847 }
842 }
848 });
843 });
849
844
850 // register submit callback on commentForm form to track TODOs
845 // register submit callback on commentForm form to track TODOs
851 window.commentFormGlobalSubmitSuccessCallback = function(){
846 window.commentFormGlobalSubmitSuccessCallback = function(){
852 refreshMergeChecks();
847 refreshMergeChecks();
853 };
848 };
854 // initial injection
849 // initial injection
855 injectCloseAction();
850 injectCloseAction();
856
851
857 ReviewerAutoComplete('#user');
852 ReviewerAutoComplete('#user');
858
853
859 })
854 })
860 </script>
855 </script>
861
856
862 </div>
857 </div>
863 </div>
858 </div>
864
859
865 </%def>
860 </%def>
@@ -1,1088 +1,1095 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 mock
21 import mock
22 import pytest
22 import pytest
23 from webob.exc import HTTPNotFound
23 from webob.exc import HTTPNotFound
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib.vcs.nodes import FileNode
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.changeset_status import ChangesetStatusModel
29 from rhodecode.model.db import (
29 from rhodecode.model.db import (
30 PullRequest, ChangesetStatus, UserLog, Notification)
30 PullRequest, ChangesetStatus, UserLog, Notification)
31 from rhodecode.model.meta import Session
31 from rhodecode.model.meta import Session
32 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.pull_request import PullRequestModel
33 from rhodecode.model.user import UserModel
33 from rhodecode.model.user import UserModel
34 from rhodecode.tests import (
34 from rhodecode.tests import (
35 assert_session_flash, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 assert_session_flash, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36 from rhodecode.tests.utils import AssertResponse
36 from rhodecode.tests.utils import AssertResponse
37
37
38
38
39 @pytest.mark.usefixtures('app', 'autologin_user')
39 @pytest.mark.usefixtures('app', 'autologin_user')
40 @pytest.mark.backends("git", "hg")
40 @pytest.mark.backends("git", "hg")
41 class TestPullrequestsController(object):
41 class TestPullrequestsController(object):
42
42
43 def test_index(self, backend):
43 def test_index(self, backend):
44 self.app.get(url(
44 self.app.get(url(
45 controller='pullrequests', action='index',
45 controller='pullrequests', action='index',
46 repo_name=backend.repo_name))
46 repo_name=backend.repo_name))
47
47
48 def test_option_menu_create_pull_request_exists(self, backend):
48 def test_option_menu_create_pull_request_exists(self, backend):
49 repo_name = backend.repo_name
49 repo_name = backend.repo_name
50 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
50 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
51
51
52 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
52 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
53 'pullrequest', repo_name=repo_name)
53 'pullrequest', repo_name=repo_name)
54 response.mustcontain(create_pr_link)
54 response.mustcontain(create_pr_link)
55
55
56 def test_create_pr_form_with_raw_commit_id(self, backend):
56 def test_create_pr_form_with_raw_commit_id(self, backend):
57 repo = backend.repo
57 repo = backend.repo
58
58
59 self.app.get(
59 self.app.get(
60 url(controller='pullrequests', action='index',
60 url(controller='pullrequests', action='index',
61 repo_name=repo.repo_name,
61 repo_name=repo.repo_name,
62 commit=repo.get_commit().raw_id),
62 commit=repo.get_commit().raw_id),
63 status=200)
63 status=200)
64
64
65 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
65 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
66 def test_show(self, pr_util, pr_merge_enabled):
66 def test_show(self, pr_util, pr_merge_enabled):
67 pull_request = pr_util.create_pull_request(
67 pull_request = pr_util.create_pull_request(
68 mergeable=pr_merge_enabled, enable_notifications=False)
68 mergeable=pr_merge_enabled, enable_notifications=False)
69
69
70 response = self.app.get(url(
70 response = self.app.get(url(
71 controller='pullrequests', action='show',
71 controller='pullrequests', action='show',
72 repo_name=pull_request.target_repo.scm_instance().name,
72 repo_name=pull_request.target_repo.scm_instance().name,
73 pull_request_id=str(pull_request.pull_request_id)))
73 pull_request_id=str(pull_request.pull_request_id)))
74
74
75 for commit_id in pull_request.revisions:
75 for commit_id in pull_request.revisions:
76 response.mustcontain(commit_id)
76 response.mustcontain(commit_id)
77
77
78 assert pull_request.target_ref_parts.type in response
78 assert pull_request.target_ref_parts.type in response
79 assert pull_request.target_ref_parts.name in response
79 assert pull_request.target_ref_parts.name in response
80 target_clone_url = pull_request.target_repo.clone_url()
80 target_clone_url = pull_request.target_repo.clone_url()
81 assert target_clone_url in response
81 assert target_clone_url in response
82
82
83 assert 'class="pull-request-merge"' in response
83 assert 'class="pull-request-merge"' in response
84 assert (
84 assert (
85 'Server-side pull request merging is disabled.'
85 'Server-side pull request merging is disabled.'
86 in response) != pr_merge_enabled
86 in response) != pr_merge_enabled
87
87
88 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
88 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
89 from rhodecode.tests.functional.test_login import login_url, logut_url
89 from rhodecode.tests.functional.test_login import login_url, logut_url
90 # Logout
90 # Logout
91 response = self.app.post(
91 response = self.app.post(
92 logut_url,
92 logut_url,
93 params={'csrf_token': csrf_token})
93 params={'csrf_token': csrf_token})
94 # Login as regular user
94 # Login as regular user
95 response = self.app.post(login_url,
95 response = self.app.post(login_url,
96 {'username': TEST_USER_REGULAR_LOGIN,
96 {'username': TEST_USER_REGULAR_LOGIN,
97 'password': 'test12'})
97 'password': 'test12'})
98
98
99 pull_request = pr_util.create_pull_request(
99 pull_request = pr_util.create_pull_request(
100 author=TEST_USER_REGULAR_LOGIN)
100 author=TEST_USER_REGULAR_LOGIN)
101
101
102 response = self.app.get(url(
102 response = self.app.get(url(
103 controller='pullrequests', action='show',
103 controller='pullrequests', action='show',
104 repo_name=pull_request.target_repo.scm_instance().name,
104 repo_name=pull_request.target_repo.scm_instance().name,
105 pull_request_id=str(pull_request.pull_request_id)))
105 pull_request_id=str(pull_request.pull_request_id)))
106
106
107 response.mustcontain('Server-side pull request merging is disabled.')
107 response.mustcontain('Server-side pull request merging is disabled.')
108
108
109 assert_response = response.assert_response()
109 assert_response = response.assert_response()
110 # for regular user without a merge permissions, we don't see it
110 # for regular user without a merge permissions, we don't see it
111 assert_response.no_element_exists('#close-pull-request-action')
111 assert_response.no_element_exists('#close-pull-request-action')
112
112
113 user_util.grant_user_permission_to_repo(
113 user_util.grant_user_permission_to_repo(
114 pull_request.target_repo,
114 pull_request.target_repo,
115 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
115 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
116 'repository.write')
116 'repository.write')
117 response = self.app.get(url(
117 response = self.app.get(url(
118 controller='pullrequests', action='show',
118 controller='pullrequests', action='show',
119 repo_name=pull_request.target_repo.scm_instance().name,
119 repo_name=pull_request.target_repo.scm_instance().name,
120 pull_request_id=str(pull_request.pull_request_id)))
120 pull_request_id=str(pull_request.pull_request_id)))
121
121
122 response.mustcontain('Server-side pull request merging is disabled.')
122 response.mustcontain('Server-side pull request merging is disabled.')
123
123
124 assert_response = response.assert_response()
124 assert_response = response.assert_response()
125 # now regular user has a merge permissions, we have CLOSE button
125 # now regular user has a merge permissions, we have CLOSE button
126 assert_response.one_element_exists('#close-pull-request-action')
126 assert_response.one_element_exists('#close-pull-request-action')
127
127
128 def test_show_invalid_commit_id(self, pr_util):
128 def test_show_invalid_commit_id(self, pr_util):
129 # Simulating invalid revisions which will cause a lookup error
129 # Simulating invalid revisions which will cause a lookup error
130 pull_request = pr_util.create_pull_request()
130 pull_request = pr_util.create_pull_request()
131 pull_request.revisions = ['invalid']
131 pull_request.revisions = ['invalid']
132 Session().add(pull_request)
132 Session().add(pull_request)
133 Session().commit()
133 Session().commit()
134
134
135 response = self.app.get(url(
135 response = self.app.get(url(
136 controller='pullrequests', action='show',
136 controller='pullrequests', action='show',
137 repo_name=pull_request.target_repo.scm_instance().name,
137 repo_name=pull_request.target_repo.scm_instance().name,
138 pull_request_id=str(pull_request.pull_request_id)))
138 pull_request_id=str(pull_request.pull_request_id)))
139
139
140 for commit_id in pull_request.revisions:
140 for commit_id in pull_request.revisions:
141 response.mustcontain(commit_id)
141 response.mustcontain(commit_id)
142
142
143 def test_show_invalid_source_reference(self, pr_util):
143 def test_show_invalid_source_reference(self, pr_util):
144 pull_request = pr_util.create_pull_request()
144 pull_request = pr_util.create_pull_request()
145 pull_request.source_ref = 'branch:b:invalid'
145 pull_request.source_ref = 'branch:b:invalid'
146 Session().add(pull_request)
146 Session().add(pull_request)
147 Session().commit()
147 Session().commit()
148
148
149 self.app.get(url(
149 self.app.get(url(
150 controller='pullrequests', action='show',
150 controller='pullrequests', action='show',
151 repo_name=pull_request.target_repo.scm_instance().name,
151 repo_name=pull_request.target_repo.scm_instance().name,
152 pull_request_id=str(pull_request.pull_request_id)))
152 pull_request_id=str(pull_request.pull_request_id)))
153
153
154 def test_edit_title_description(self, pr_util, csrf_token):
154 def test_edit_title_description(self, pr_util, csrf_token):
155 pull_request = pr_util.create_pull_request()
155 pull_request = pr_util.create_pull_request()
156 pull_request_id = pull_request.pull_request_id
156 pull_request_id = pull_request.pull_request_id
157
157
158 response = self.app.post(
158 response = self.app.post(
159 url(controller='pullrequests', action='update',
159 url(controller='pullrequests', action='update',
160 repo_name=pull_request.target_repo.repo_name,
160 repo_name=pull_request.target_repo.repo_name,
161 pull_request_id=str(pull_request_id)),
161 pull_request_id=str(pull_request_id)),
162 params={
162 params={
163 'edit_pull_request': 'true',
163 'edit_pull_request': 'true',
164 '_method': 'put',
164 '_method': 'put',
165 'title': 'New title',
165 'title': 'New title',
166 'description': 'New description',
166 'description': 'New description',
167 'csrf_token': csrf_token})
167 'csrf_token': csrf_token})
168
168
169 assert_session_flash(
169 assert_session_flash(
170 response, u'Pull request title & description updated.',
170 response, u'Pull request title & description updated.',
171 category='success')
171 category='success')
172
172
173 pull_request = PullRequest.get(pull_request_id)
173 pull_request = PullRequest.get(pull_request_id)
174 assert pull_request.title == 'New title'
174 assert pull_request.title == 'New title'
175 assert pull_request.description == 'New description'
175 assert pull_request.description == 'New description'
176
176
177 def test_edit_title_description_closed(self, pr_util, csrf_token):
177 def test_edit_title_description_closed(self, pr_util, csrf_token):
178 pull_request = pr_util.create_pull_request()
178 pull_request = pr_util.create_pull_request()
179 pull_request_id = pull_request.pull_request_id
179 pull_request_id = pull_request.pull_request_id
180 pr_util.close()
180 pr_util.close()
181
181
182 response = self.app.post(
182 response = self.app.post(
183 url(controller='pullrequests', action='update',
183 url(controller='pullrequests', action='update',
184 repo_name=pull_request.target_repo.repo_name,
184 repo_name=pull_request.target_repo.repo_name,
185 pull_request_id=str(pull_request_id)),
185 pull_request_id=str(pull_request_id)),
186 params={
186 params={
187 'edit_pull_request': 'true',
187 'edit_pull_request': 'true',
188 '_method': 'put',
188 '_method': 'put',
189 'title': 'New title',
189 'title': 'New title',
190 'description': 'New description',
190 'description': 'New description',
191 'csrf_token': csrf_token})
191 'csrf_token': csrf_token})
192
192
193 assert_session_flash(
193 assert_session_flash(
194 response, u'Cannot update closed pull requests.',
194 response, u'Cannot update closed pull requests.',
195 category='error')
195 category='error')
196
196
197 def test_update_invalid_source_reference(self, pr_util, csrf_token):
197 def test_update_invalid_source_reference(self, pr_util, csrf_token):
198 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
198 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
199
199
200 pull_request = pr_util.create_pull_request()
200 pull_request = pr_util.create_pull_request()
201 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
201 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
202 Session().add(pull_request)
202 Session().add(pull_request)
203 Session().commit()
203 Session().commit()
204
204
205 pull_request_id = pull_request.pull_request_id
205 pull_request_id = pull_request.pull_request_id
206
206
207 response = self.app.post(
207 response = self.app.post(
208 url(controller='pullrequests', action='update',
208 url(controller='pullrequests', action='update',
209 repo_name=pull_request.target_repo.repo_name,
209 repo_name=pull_request.target_repo.repo_name,
210 pull_request_id=str(pull_request_id)),
210 pull_request_id=str(pull_request_id)),
211 params={'update_commits': 'true', '_method': 'put',
211 params={'update_commits': 'true', '_method': 'put',
212 'csrf_token': csrf_token})
212 'csrf_token': csrf_token})
213
213
214 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
214 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
215 UpdateFailureReason.MISSING_SOURCE_REF]
215 UpdateFailureReason.MISSING_SOURCE_REF]
216 assert_session_flash(response, expected_msg, category='error')
216 assert_session_flash(response, expected_msg, category='error')
217
217
218 def test_missing_target_reference(self, pr_util, csrf_token):
218 def test_missing_target_reference(self, pr_util, csrf_token):
219 from rhodecode.lib.vcs.backends.base import MergeFailureReason
219 from rhodecode.lib.vcs.backends.base import MergeFailureReason
220 pull_request = pr_util.create_pull_request(
220 pull_request = pr_util.create_pull_request(
221 approved=True, mergeable=True)
221 approved=True, mergeable=True)
222 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
222 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
223 Session().add(pull_request)
223 Session().add(pull_request)
224 Session().commit()
224 Session().commit()
225
225
226 pull_request_id = pull_request.pull_request_id
226 pull_request_id = pull_request.pull_request_id
227 pull_request_url = url(
227 pull_request_url = url(
228 controller='pullrequests', action='show',
228 controller='pullrequests', action='show',
229 repo_name=pull_request.target_repo.repo_name,
229 repo_name=pull_request.target_repo.repo_name,
230 pull_request_id=str(pull_request_id))
230 pull_request_id=str(pull_request_id))
231
231
232 response = self.app.get(pull_request_url)
232 response = self.app.get(pull_request_url)
233
233
234 assertr = AssertResponse(response)
234 assertr = AssertResponse(response)
235 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
235 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
236 MergeFailureReason.MISSING_TARGET_REF]
236 MergeFailureReason.MISSING_TARGET_REF]
237 assertr.element_contains(
237 assertr.element_contains(
238 'span[data-role="merge-message"]', str(expected_msg))
238 'span[data-role="merge-message"]', str(expected_msg))
239
239
240 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
240 def test_comment_and_close_pull_request_custom_message_approved(
241 self, pr_util, csrf_token, xhr_header):
242
241 pull_request = pr_util.create_pull_request(approved=True)
243 pull_request = pr_util.create_pull_request(approved=True)
242 pull_request_id = pull_request.pull_request_id
244 pull_request_id = pull_request.pull_request_id
243 author = pull_request.user_id
245 author = pull_request.user_id
244 repo = pull_request.target_repo.repo_id
246 repo = pull_request.target_repo.repo_id
245
247
246 self.app.post(
248 self.app.post(
247 url(controller='pullrequests',
249 url(controller='pullrequests',
248 action='comment',
250 action='comment',
249 repo_name=pull_request.target_repo.scm_instance().name,
251 repo_name=pull_request.target_repo.scm_instance().name,
250 pull_request_id=str(pull_request_id)),
252 pull_request_id=str(pull_request_id)),
251 params={
253 params={
252 'changeset_status': ChangesetStatus.STATUS_APPROVED,
253 'close_pull_request': '1',
254 'close_pull_request': '1',
254 'text': 'Closing a PR',
255 'text': 'Closing a PR',
255 'csrf_token': csrf_token},
256 'csrf_token': csrf_token},
256 status=302)
257 extra_environ=xhr_header,)
257
258
258 action = 'user_closed_pull_request:%d' % pull_request_id
259 action = 'user_closed_pull_request:%d' % pull_request_id
259 journal = UserLog.query()\
260 journal = UserLog.query()\
260 .filter(UserLog.user_id == author)\
261 .filter(UserLog.user_id == author)\
261 .filter(UserLog.repository_id == repo)\
262 .filter(UserLog.repository_id == repo)\
262 .filter(UserLog.action == action)\
263 .filter(UserLog.action == action)\
263 .all()
264 .all()
264 assert len(journal) == 1
265 assert len(journal) == 1
265
266
266 pull_request = PullRequest.get(pull_request_id)
267 pull_request = PullRequest.get(pull_request_id)
267 assert pull_request.is_closed()
268 assert pull_request.is_closed()
268
269
269 # check only the latest status, not the review status
270 # check only the latest status, not the review status
270 status = ChangesetStatusModel().get_status(
271 status = ChangesetStatusModel().get_status(
271 pull_request.source_repo, pull_request=pull_request)
272 pull_request.source_repo, pull_request=pull_request)
272 assert status == ChangesetStatus.STATUS_APPROVED
273 assert status == ChangesetStatus.STATUS_APPROVED
273
274 assert pull_request.comments[-1].text == 'Closing a PR'
274 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
275 pull_request = pr_util.create_pull_request()
276 pull_request_id = pull_request.pull_request_id
277 response = self.app.post(
278 url(controller='pullrequests',
279 action='update',
280 repo_name=pull_request.target_repo.scm_instance().name,
281 pull_request_id=str(pull_request.pull_request_id)),
282 params={'close_pull_request': 'true', '_method': 'put',
283 'csrf_token': csrf_token})
284
275
285 pull_request = PullRequest.get(pull_request_id)
276 def test_comment_force_close_pull_request_rejected(
286
277 self, pr_util, csrf_token, xhr_header):
287 assert response.json is True
288 assert pull_request.is_closed()
289
290 # check only the latest status, not the review status
291 status = ChangesetStatusModel().get_status(
292 pull_request.source_repo, pull_request=pull_request)
293 assert status == ChangesetStatus.STATUS_REJECTED
294
295 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
296 pull_request = pr_util.create_pull_request()
278 pull_request = pr_util.create_pull_request()
297 pull_request_id = pull_request.pull_request_id
279 pull_request_id = pull_request.pull_request_id
298 PullRequestModel().update_reviewers(
280 PullRequestModel().update_reviewers(
299 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)])
281 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)])
300 author = pull_request.user_id
282 author = pull_request.user_id
301 repo = pull_request.target_repo.repo_id
283 repo = pull_request.target_repo.repo_id
284
302 self.app.post(
285 self.app.post(
303 url(controller='pullrequests',
286 url(controller='pullrequests',
304 action='comment',
287 action='comment',
305 repo_name=pull_request.target_repo.scm_instance().name,
288 repo_name=pull_request.target_repo.scm_instance().name,
306 pull_request_id=str(pull_request_id)),
289 pull_request_id=str(pull_request_id)),
307 params={
290 params={
308 'changeset_status': 'rejected',
309 'close_pull_request': '1',
291 'close_pull_request': '1',
310 'csrf_token': csrf_token},
292 'csrf_token': csrf_token},
311 status=302)
293 extra_environ=xhr_header)
312
294
313 pull_request = PullRequest.get(pull_request_id)
295 pull_request = PullRequest.get(pull_request_id)
314
296
315 action = 'user_closed_pull_request:%d' % pull_request_id
297 action = 'user_closed_pull_request:%d' % pull_request_id
316 journal = UserLog.query().filter(
298 journal = UserLog.query().filter(
317 UserLog.user_id == author,
299 UserLog.user_id == author,
318 UserLog.repository_id == repo,
300 UserLog.repository_id == repo,
319 UserLog.action == action).all()
301 UserLog.action == action).all()
320 assert len(journal) == 1
302 assert len(journal) == 1
321
303
322 # check only the latest status, not the review status
304 # check only the latest status, not the review status
323 status = ChangesetStatusModel().get_status(
305 status = ChangesetStatusModel().get_status(
324 pull_request.source_repo, pull_request=pull_request)
306 pull_request.source_repo, pull_request=pull_request)
325 assert status == ChangesetStatus.STATUS_REJECTED
307 assert status == ChangesetStatus.STATUS_REJECTED
326
308
309 def test_comment_and_close_pull_request(
310 self, pr_util, csrf_token, xhr_header):
311 pull_request = pr_util.create_pull_request()
312 pull_request_id = pull_request.pull_request_id
313
314 response = self.app.post(
315 url(controller='pullrequests',
316 action='comment',
317 repo_name=pull_request.target_repo.scm_instance().name,
318 pull_request_id=str(pull_request.pull_request_id)),
319 params={
320 'close_pull_request': 'true',
321 'csrf_token': csrf_token},
322 extra_environ=xhr_header)
323
324 assert response.json
325
326 pull_request = PullRequest.get(pull_request_id)
327 assert pull_request.is_closed()
328
329 # check only the latest status, not the review status
330 status = ChangesetStatusModel().get_status(
331 pull_request.source_repo, pull_request=pull_request)
332 assert status == ChangesetStatus.STATUS_REJECTED
333
327 def test_create_pull_request(self, backend, csrf_token):
334 def test_create_pull_request(self, backend, csrf_token):
328 commits = [
335 commits = [
329 {'message': 'ancestor'},
336 {'message': 'ancestor'},
330 {'message': 'change'},
337 {'message': 'change'},
331 {'message': 'change2'},
338 {'message': 'change2'},
332 ]
339 ]
333 commit_ids = backend.create_master_repo(commits)
340 commit_ids = backend.create_master_repo(commits)
334 target = backend.create_repo(heads=['ancestor'])
341 target = backend.create_repo(heads=['ancestor'])
335 source = backend.create_repo(heads=['change2'])
342 source = backend.create_repo(heads=['change2'])
336
343
337 response = self.app.post(
344 response = self.app.post(
338 url(
345 url(
339 controller='pullrequests',
346 controller='pullrequests',
340 action='create',
347 action='create',
341 repo_name=source.repo_name
348 repo_name=source.repo_name
342 ),
349 ),
343 [
350 [
344 ('source_repo', source.repo_name),
351 ('source_repo', source.repo_name),
345 ('source_ref', 'branch:default:' + commit_ids['change2']),
352 ('source_ref', 'branch:default:' + commit_ids['change2']),
346 ('target_repo', target.repo_name),
353 ('target_repo', target.repo_name),
347 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
354 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
348 ('common_ancestor', commit_ids['ancestor']),
355 ('common_ancestor', commit_ids['ancestor']),
349 ('pullrequest_desc', 'Description'),
356 ('pullrequest_desc', 'Description'),
350 ('pullrequest_title', 'Title'),
357 ('pullrequest_title', 'Title'),
351 ('__start__', 'review_members:sequence'),
358 ('__start__', 'review_members:sequence'),
352 ('__start__', 'reviewer:mapping'),
359 ('__start__', 'reviewer:mapping'),
353 ('user_id', '1'),
360 ('user_id', '1'),
354 ('__start__', 'reasons:sequence'),
361 ('__start__', 'reasons:sequence'),
355 ('reason', 'Some reason'),
362 ('reason', 'Some reason'),
356 ('__end__', 'reasons:sequence'),
363 ('__end__', 'reasons:sequence'),
357 ('mandatory', 'False'),
364 ('mandatory', 'False'),
358 ('__end__', 'reviewer:mapping'),
365 ('__end__', 'reviewer:mapping'),
359 ('__end__', 'review_members:sequence'),
366 ('__end__', 'review_members:sequence'),
360 ('__start__', 'revisions:sequence'),
367 ('__start__', 'revisions:sequence'),
361 ('revisions', commit_ids['change']),
368 ('revisions', commit_ids['change']),
362 ('revisions', commit_ids['change2']),
369 ('revisions', commit_ids['change2']),
363 ('__end__', 'revisions:sequence'),
370 ('__end__', 'revisions:sequence'),
364 ('user', ''),
371 ('user', ''),
365 ('csrf_token', csrf_token),
372 ('csrf_token', csrf_token),
366 ],
373 ],
367 status=302)
374 status=302)
368
375
369 location = response.headers['Location']
376 location = response.headers['Location']
370 pull_request_id = location.rsplit('/', 1)[1]
377 pull_request_id = location.rsplit('/', 1)[1]
371 assert pull_request_id != 'new'
378 assert pull_request_id != 'new'
372 pull_request = PullRequest.get(int(pull_request_id))
379 pull_request = PullRequest.get(int(pull_request_id))
373
380
374 # check that we have now both revisions
381 # check that we have now both revisions
375 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
382 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
376 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
383 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
377 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
384 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
378 assert pull_request.target_ref == expected_target_ref
385 assert pull_request.target_ref == expected_target_ref
379
386
380 def test_reviewer_notifications(self, backend, csrf_token):
387 def test_reviewer_notifications(self, backend, csrf_token):
381 # We have to use the app.post for this test so it will create the
388 # We have to use the app.post for this test so it will create the
382 # notifications properly with the new PR
389 # notifications properly with the new PR
383 commits = [
390 commits = [
384 {'message': 'ancestor',
391 {'message': 'ancestor',
385 'added': [FileNode('file_A', content='content_of_ancestor')]},
392 'added': [FileNode('file_A', content='content_of_ancestor')]},
386 {'message': 'change',
393 {'message': 'change',
387 'added': [FileNode('file_a', content='content_of_change')]},
394 'added': [FileNode('file_a', content='content_of_change')]},
388 {'message': 'change-child'},
395 {'message': 'change-child'},
389 {'message': 'ancestor-child', 'parents': ['ancestor'],
396 {'message': 'ancestor-child', 'parents': ['ancestor'],
390 'added': [
397 'added': [
391 FileNode('file_B', content='content_of_ancestor_child')]},
398 FileNode('file_B', content='content_of_ancestor_child')]},
392 {'message': 'ancestor-child-2'},
399 {'message': 'ancestor-child-2'},
393 ]
400 ]
394 commit_ids = backend.create_master_repo(commits)
401 commit_ids = backend.create_master_repo(commits)
395 target = backend.create_repo(heads=['ancestor-child'])
402 target = backend.create_repo(heads=['ancestor-child'])
396 source = backend.create_repo(heads=['change'])
403 source = backend.create_repo(heads=['change'])
397
404
398 response = self.app.post(
405 response = self.app.post(
399 url(
406 url(
400 controller='pullrequests',
407 controller='pullrequests',
401 action='create',
408 action='create',
402 repo_name=source.repo_name
409 repo_name=source.repo_name
403 ),
410 ),
404 [
411 [
405 ('source_repo', source.repo_name),
412 ('source_repo', source.repo_name),
406 ('source_ref', 'branch:default:' + commit_ids['change']),
413 ('source_ref', 'branch:default:' + commit_ids['change']),
407 ('target_repo', target.repo_name),
414 ('target_repo', target.repo_name),
408 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
415 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
409 ('common_ancestor', commit_ids['ancestor']),
416 ('common_ancestor', commit_ids['ancestor']),
410 ('pullrequest_desc', 'Description'),
417 ('pullrequest_desc', 'Description'),
411 ('pullrequest_title', 'Title'),
418 ('pullrequest_title', 'Title'),
412 ('__start__', 'review_members:sequence'),
419 ('__start__', 'review_members:sequence'),
413 ('__start__', 'reviewer:mapping'),
420 ('__start__', 'reviewer:mapping'),
414 ('user_id', '2'),
421 ('user_id', '2'),
415 ('__start__', 'reasons:sequence'),
422 ('__start__', 'reasons:sequence'),
416 ('reason', 'Some reason'),
423 ('reason', 'Some reason'),
417 ('__end__', 'reasons:sequence'),
424 ('__end__', 'reasons:sequence'),
418 ('mandatory', 'False'),
425 ('mandatory', 'False'),
419 ('__end__', 'reviewer:mapping'),
426 ('__end__', 'reviewer:mapping'),
420 ('__end__', 'review_members:sequence'),
427 ('__end__', 'review_members:sequence'),
421 ('__start__', 'revisions:sequence'),
428 ('__start__', 'revisions:sequence'),
422 ('revisions', commit_ids['change']),
429 ('revisions', commit_ids['change']),
423 ('__end__', 'revisions:sequence'),
430 ('__end__', 'revisions:sequence'),
424 ('user', ''),
431 ('user', ''),
425 ('csrf_token', csrf_token),
432 ('csrf_token', csrf_token),
426 ],
433 ],
427 status=302)
434 status=302)
428
435
429 location = response.headers['Location']
436 location = response.headers['Location']
430
437
431 pull_request_id = location.rsplit('/', 1)[1]
438 pull_request_id = location.rsplit('/', 1)[1]
432 assert pull_request_id != 'new'
439 assert pull_request_id != 'new'
433 pull_request = PullRequest.get(int(pull_request_id))
440 pull_request = PullRequest.get(int(pull_request_id))
434
441
435 # Check that a notification was made
442 # Check that a notification was made
436 notifications = Notification.query()\
443 notifications = Notification.query()\
437 .filter(Notification.created_by == pull_request.author.user_id,
444 .filter(Notification.created_by == pull_request.author.user_id,
438 Notification.type_ == Notification.TYPE_PULL_REQUEST,
445 Notification.type_ == Notification.TYPE_PULL_REQUEST,
439 Notification.subject.contains(
446 Notification.subject.contains(
440 "wants you to review pull request #%s" % pull_request_id))
447 "wants you to review pull request #%s" % pull_request_id))
441 assert len(notifications.all()) == 1
448 assert len(notifications.all()) == 1
442
449
443 # Change reviewers and check that a notification was made
450 # Change reviewers and check that a notification was made
444 PullRequestModel().update_reviewers(
451 PullRequestModel().update_reviewers(
445 pull_request.pull_request_id, [(1, [], False)])
452 pull_request.pull_request_id, [(1, [], False)])
446 assert len(notifications.all()) == 2
453 assert len(notifications.all()) == 2
447
454
448 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
455 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
449 csrf_token):
456 csrf_token):
450 commits = [
457 commits = [
451 {'message': 'ancestor',
458 {'message': 'ancestor',
452 'added': [FileNode('file_A', content='content_of_ancestor')]},
459 'added': [FileNode('file_A', content='content_of_ancestor')]},
453 {'message': 'change',
460 {'message': 'change',
454 'added': [FileNode('file_a', content='content_of_change')]},
461 'added': [FileNode('file_a', content='content_of_change')]},
455 {'message': 'change-child'},
462 {'message': 'change-child'},
456 {'message': 'ancestor-child', 'parents': ['ancestor'],
463 {'message': 'ancestor-child', 'parents': ['ancestor'],
457 'added': [
464 'added': [
458 FileNode('file_B', content='content_of_ancestor_child')]},
465 FileNode('file_B', content='content_of_ancestor_child')]},
459 {'message': 'ancestor-child-2'},
466 {'message': 'ancestor-child-2'},
460 ]
467 ]
461 commit_ids = backend.create_master_repo(commits)
468 commit_ids = backend.create_master_repo(commits)
462 target = backend.create_repo(heads=['ancestor-child'])
469 target = backend.create_repo(heads=['ancestor-child'])
463 source = backend.create_repo(heads=['change'])
470 source = backend.create_repo(heads=['change'])
464
471
465 response = self.app.post(
472 response = self.app.post(
466 url(
473 url(
467 controller='pullrequests',
474 controller='pullrequests',
468 action='create',
475 action='create',
469 repo_name=source.repo_name
476 repo_name=source.repo_name
470 ),
477 ),
471 [
478 [
472 ('source_repo', source.repo_name),
479 ('source_repo', source.repo_name),
473 ('source_ref', 'branch:default:' + commit_ids['change']),
480 ('source_ref', 'branch:default:' + commit_ids['change']),
474 ('target_repo', target.repo_name),
481 ('target_repo', target.repo_name),
475 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
482 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
476 ('common_ancestor', commit_ids['ancestor']),
483 ('common_ancestor', commit_ids['ancestor']),
477 ('pullrequest_desc', 'Description'),
484 ('pullrequest_desc', 'Description'),
478 ('pullrequest_title', 'Title'),
485 ('pullrequest_title', 'Title'),
479 ('__start__', 'review_members:sequence'),
486 ('__start__', 'review_members:sequence'),
480 ('__start__', 'reviewer:mapping'),
487 ('__start__', 'reviewer:mapping'),
481 ('user_id', '1'),
488 ('user_id', '1'),
482 ('__start__', 'reasons:sequence'),
489 ('__start__', 'reasons:sequence'),
483 ('reason', 'Some reason'),
490 ('reason', 'Some reason'),
484 ('__end__', 'reasons:sequence'),
491 ('__end__', 'reasons:sequence'),
485 ('mandatory', 'False'),
492 ('mandatory', 'False'),
486 ('__end__', 'reviewer:mapping'),
493 ('__end__', 'reviewer:mapping'),
487 ('__end__', 'review_members:sequence'),
494 ('__end__', 'review_members:sequence'),
488 ('__start__', 'revisions:sequence'),
495 ('__start__', 'revisions:sequence'),
489 ('revisions', commit_ids['change']),
496 ('revisions', commit_ids['change']),
490 ('__end__', 'revisions:sequence'),
497 ('__end__', 'revisions:sequence'),
491 ('user', ''),
498 ('user', ''),
492 ('csrf_token', csrf_token),
499 ('csrf_token', csrf_token),
493 ],
500 ],
494 status=302)
501 status=302)
495
502
496 location = response.headers['Location']
503 location = response.headers['Location']
497
504
498 pull_request_id = location.rsplit('/', 1)[1]
505 pull_request_id = location.rsplit('/', 1)[1]
499 assert pull_request_id != 'new'
506 assert pull_request_id != 'new'
500 pull_request = PullRequest.get(int(pull_request_id))
507 pull_request = PullRequest.get(int(pull_request_id))
501
508
502 # target_ref has to point to the ancestor's commit_id in order to
509 # target_ref has to point to the ancestor's commit_id in order to
503 # show the correct diff
510 # show the correct diff
504 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
511 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
505 assert pull_request.target_ref == expected_target_ref
512 assert pull_request.target_ref == expected_target_ref
506
513
507 # Check generated diff contents
514 # Check generated diff contents
508 response = response.follow()
515 response = response.follow()
509 assert 'content_of_ancestor' not in response.body
516 assert 'content_of_ancestor' not in response.body
510 assert 'content_of_ancestor-child' not in response.body
517 assert 'content_of_ancestor-child' not in response.body
511 assert 'content_of_change' in response.body
518 assert 'content_of_change' in response.body
512
519
513 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
520 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
514 # Clear any previous calls to rcextensions
521 # Clear any previous calls to rcextensions
515 rhodecode.EXTENSIONS.calls.clear()
522 rhodecode.EXTENSIONS.calls.clear()
516
523
517 pull_request = pr_util.create_pull_request(
524 pull_request = pr_util.create_pull_request(
518 approved=True, mergeable=True)
525 approved=True, mergeable=True)
519 pull_request_id = pull_request.pull_request_id
526 pull_request_id = pull_request.pull_request_id
520 repo_name = pull_request.target_repo.scm_instance().name,
527 repo_name = pull_request.target_repo.scm_instance().name,
521
528
522 response = self.app.post(
529 response = self.app.post(
523 url(controller='pullrequests',
530 url(controller='pullrequests',
524 action='merge',
531 action='merge',
525 repo_name=str(repo_name[0]),
532 repo_name=str(repo_name[0]),
526 pull_request_id=str(pull_request_id)),
533 pull_request_id=str(pull_request_id)),
527 params={'csrf_token': csrf_token}).follow()
534 params={'csrf_token': csrf_token}).follow()
528
535
529 pull_request = PullRequest.get(pull_request_id)
536 pull_request = PullRequest.get(pull_request_id)
530
537
531 assert response.status_int == 200
538 assert response.status_int == 200
532 assert pull_request.is_closed()
539 assert pull_request.is_closed()
533 assert_pull_request_status(
540 assert_pull_request_status(
534 pull_request, ChangesetStatus.STATUS_APPROVED)
541 pull_request, ChangesetStatus.STATUS_APPROVED)
535
542
536 # Check the relevant log entries were added
543 # Check the relevant log entries were added
537 user_logs = UserLog.query() \
544 user_logs = UserLog.query() \
538 .filter(UserLog.version == UserLog.VERSION_1) \
545 .filter(UserLog.version == UserLog.VERSION_1) \
539 .order_by('-user_log_id').limit(3)
546 .order_by('-user_log_id').limit(3)
540 actions = [log.action for log in user_logs]
547 actions = [log.action for log in user_logs]
541 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
548 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
542 expected_actions = [
549 expected_actions = [
543 u'user_closed_pull_request:%d' % pull_request_id,
550 u'user_closed_pull_request:%d' % pull_request_id,
544 u'user_merged_pull_request:%d' % pull_request_id,
551 u'user_merged_pull_request:%d' % pull_request_id,
545 # The action below reflect that the post push actions were executed
552 # The action below reflect that the post push actions were executed
546 u'user_commented_pull_request:%d' % pull_request_id,
553 u'user_commented_pull_request:%d' % pull_request_id,
547 ]
554 ]
548 assert actions == expected_actions
555 assert actions == expected_actions
549
556
550 user_logs = UserLog.query() \
557 user_logs = UserLog.query() \
551 .filter(UserLog.version == UserLog.VERSION_2) \
558 .filter(UserLog.version == UserLog.VERSION_2) \
552 .order_by('-user_log_id').limit(1)
559 .order_by('-user_log_id').limit(1)
553 actions = [log.action for log in user_logs]
560 actions = [log.action for log in user_logs]
554 assert actions == ['user.push']
561 assert actions == ['user.push']
555 assert user_logs[0].action_data['commit_ids'] == pr_commit_ids
562 assert user_logs[0].action_data['commit_ids'] == pr_commit_ids
556
563
557 # Check post_push rcextension was really executed
564 # Check post_push rcextension was really executed
558 push_calls = rhodecode.EXTENSIONS.calls['post_push']
565 push_calls = rhodecode.EXTENSIONS.calls['post_push']
559 assert len(push_calls) == 1
566 assert len(push_calls) == 1
560 unused_last_call_args, last_call_kwargs = push_calls[0]
567 unused_last_call_args, last_call_kwargs = push_calls[0]
561 assert last_call_kwargs['action'] == 'push'
568 assert last_call_kwargs['action'] == 'push'
562 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
569 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
563
570
564 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
571 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
565 pull_request = pr_util.create_pull_request(mergeable=False)
572 pull_request = pr_util.create_pull_request(mergeable=False)
566 pull_request_id = pull_request.pull_request_id
573 pull_request_id = pull_request.pull_request_id
567 pull_request = PullRequest.get(pull_request_id)
574 pull_request = PullRequest.get(pull_request_id)
568
575
569 response = self.app.post(
576 response = self.app.post(
570 url(controller='pullrequests',
577 url(controller='pullrequests',
571 action='merge',
578 action='merge',
572 repo_name=pull_request.target_repo.scm_instance().name,
579 repo_name=pull_request.target_repo.scm_instance().name,
573 pull_request_id=str(pull_request.pull_request_id)),
580 pull_request_id=str(pull_request.pull_request_id)),
574 params={'csrf_token': csrf_token}).follow()
581 params={'csrf_token': csrf_token}).follow()
575
582
576 assert response.status_int == 200
583 assert response.status_int == 200
577 response.mustcontain(
584 response.mustcontain(
578 'Merge is not currently possible because of below failed checks.')
585 'Merge is not currently possible because of below failed checks.')
579 response.mustcontain('Server-side pull request merging is disabled.')
586 response.mustcontain('Server-side pull request merging is disabled.')
580
587
581 @pytest.mark.skip_backends('svn')
588 @pytest.mark.skip_backends('svn')
582 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
589 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
583 pull_request = pr_util.create_pull_request(mergeable=True)
590 pull_request = pr_util.create_pull_request(mergeable=True)
584 pull_request_id = pull_request.pull_request_id
591 pull_request_id = pull_request.pull_request_id
585 repo_name = pull_request.target_repo.scm_instance().name,
592 repo_name = pull_request.target_repo.scm_instance().name,
586
593
587 response = self.app.post(
594 response = self.app.post(
588 url(controller='pullrequests',
595 url(controller='pullrequests',
589 action='merge',
596 action='merge',
590 repo_name=str(repo_name[0]),
597 repo_name=str(repo_name[0]),
591 pull_request_id=str(pull_request_id)),
598 pull_request_id=str(pull_request_id)),
592 params={'csrf_token': csrf_token}).follow()
599 params={'csrf_token': csrf_token}).follow()
593
600
594 assert response.status_int == 200
601 assert response.status_int == 200
595
602
596 response.mustcontain(
603 response.mustcontain(
597 'Merge is not currently possible because of below failed checks.')
604 'Merge is not currently possible because of below failed checks.')
598 response.mustcontain('Pull request reviewer approval is pending.')
605 response.mustcontain('Pull request reviewer approval is pending.')
599
606
600 def test_update_source_revision(self, backend, csrf_token):
607 def test_update_source_revision(self, backend, csrf_token):
601 commits = [
608 commits = [
602 {'message': 'ancestor'},
609 {'message': 'ancestor'},
603 {'message': 'change'},
610 {'message': 'change'},
604 {'message': 'change-2'},
611 {'message': 'change-2'},
605 ]
612 ]
606 commit_ids = backend.create_master_repo(commits)
613 commit_ids = backend.create_master_repo(commits)
607 target = backend.create_repo(heads=['ancestor'])
614 target = backend.create_repo(heads=['ancestor'])
608 source = backend.create_repo(heads=['change'])
615 source = backend.create_repo(heads=['change'])
609
616
610 # create pr from a in source to A in target
617 # create pr from a in source to A in target
611 pull_request = PullRequest()
618 pull_request = PullRequest()
612 pull_request.source_repo = source
619 pull_request.source_repo = source
613 # TODO: johbo: Make sure that we write the source ref this way!
620 # TODO: johbo: Make sure that we write the source ref this way!
614 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
621 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
615 branch=backend.default_branch_name, commit_id=commit_ids['change'])
622 branch=backend.default_branch_name, commit_id=commit_ids['change'])
616 pull_request.target_repo = target
623 pull_request.target_repo = target
617
624
618 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
625 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
619 branch=backend.default_branch_name,
626 branch=backend.default_branch_name,
620 commit_id=commit_ids['ancestor'])
627 commit_id=commit_ids['ancestor'])
621 pull_request.revisions = [commit_ids['change']]
628 pull_request.revisions = [commit_ids['change']]
622 pull_request.title = u"Test"
629 pull_request.title = u"Test"
623 pull_request.description = u"Description"
630 pull_request.description = u"Description"
624 pull_request.author = UserModel().get_by_username(
631 pull_request.author = UserModel().get_by_username(
625 TEST_USER_ADMIN_LOGIN)
632 TEST_USER_ADMIN_LOGIN)
626 Session().add(pull_request)
633 Session().add(pull_request)
627 Session().commit()
634 Session().commit()
628 pull_request_id = pull_request.pull_request_id
635 pull_request_id = pull_request.pull_request_id
629
636
630 # source has ancestor - change - change-2
637 # source has ancestor - change - change-2
631 backend.pull_heads(source, heads=['change-2'])
638 backend.pull_heads(source, heads=['change-2'])
632
639
633 # update PR
640 # update PR
634 self.app.post(
641 self.app.post(
635 url(controller='pullrequests', action='update',
642 url(controller='pullrequests', action='update',
636 repo_name=target.repo_name,
643 repo_name=target.repo_name,
637 pull_request_id=str(pull_request_id)),
644 pull_request_id=str(pull_request_id)),
638 params={'update_commits': 'true', '_method': 'put',
645 params={'update_commits': 'true', '_method': 'put',
639 'csrf_token': csrf_token})
646 'csrf_token': csrf_token})
640
647
641 # check that we have now both revisions
648 # check that we have now both revisions
642 pull_request = PullRequest.get(pull_request_id)
649 pull_request = PullRequest.get(pull_request_id)
643 assert pull_request.revisions == [
650 assert pull_request.revisions == [
644 commit_ids['change-2'], commit_ids['change']]
651 commit_ids['change-2'], commit_ids['change']]
645
652
646 # TODO: johbo: this should be a test on its own
653 # TODO: johbo: this should be a test on its own
647 response = self.app.get(url(
654 response = self.app.get(url(
648 controller='pullrequests', action='index',
655 controller='pullrequests', action='index',
649 repo_name=target.repo_name))
656 repo_name=target.repo_name))
650 assert response.status_int == 200
657 assert response.status_int == 200
651 assert 'Pull request updated to' in response.body
658 assert 'Pull request updated to' in response.body
652 assert 'with 1 added, 0 removed commits.' in response.body
659 assert 'with 1 added, 0 removed commits.' in response.body
653
660
654 def test_update_target_revision(self, backend, csrf_token):
661 def test_update_target_revision(self, backend, csrf_token):
655 commits = [
662 commits = [
656 {'message': 'ancestor'},
663 {'message': 'ancestor'},
657 {'message': 'change'},
664 {'message': 'change'},
658 {'message': 'ancestor-new', 'parents': ['ancestor']},
665 {'message': 'ancestor-new', 'parents': ['ancestor']},
659 {'message': 'change-rebased'},
666 {'message': 'change-rebased'},
660 ]
667 ]
661 commit_ids = backend.create_master_repo(commits)
668 commit_ids = backend.create_master_repo(commits)
662 target = backend.create_repo(heads=['ancestor'])
669 target = backend.create_repo(heads=['ancestor'])
663 source = backend.create_repo(heads=['change'])
670 source = backend.create_repo(heads=['change'])
664
671
665 # create pr from a in source to A in target
672 # create pr from a in source to A in target
666 pull_request = PullRequest()
673 pull_request = PullRequest()
667 pull_request.source_repo = source
674 pull_request.source_repo = source
668 # TODO: johbo: Make sure that we write the source ref this way!
675 # TODO: johbo: Make sure that we write the source ref this way!
669 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
676 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
670 branch=backend.default_branch_name, commit_id=commit_ids['change'])
677 branch=backend.default_branch_name, commit_id=commit_ids['change'])
671 pull_request.target_repo = target
678 pull_request.target_repo = target
672 # TODO: johbo: Target ref should be branch based, since tip can jump
679 # TODO: johbo: Target ref should be branch based, since tip can jump
673 # from branch to branch
680 # from branch to branch
674 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
681 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
675 branch=backend.default_branch_name,
682 branch=backend.default_branch_name,
676 commit_id=commit_ids['ancestor'])
683 commit_id=commit_ids['ancestor'])
677 pull_request.revisions = [commit_ids['change']]
684 pull_request.revisions = [commit_ids['change']]
678 pull_request.title = u"Test"
685 pull_request.title = u"Test"
679 pull_request.description = u"Description"
686 pull_request.description = u"Description"
680 pull_request.author = UserModel().get_by_username(
687 pull_request.author = UserModel().get_by_username(
681 TEST_USER_ADMIN_LOGIN)
688 TEST_USER_ADMIN_LOGIN)
682 Session().add(pull_request)
689 Session().add(pull_request)
683 Session().commit()
690 Session().commit()
684 pull_request_id = pull_request.pull_request_id
691 pull_request_id = pull_request.pull_request_id
685
692
686 # target has ancestor - ancestor-new
693 # target has ancestor - ancestor-new
687 # source has ancestor - ancestor-new - change-rebased
694 # source has ancestor - ancestor-new - change-rebased
688 backend.pull_heads(target, heads=['ancestor-new'])
695 backend.pull_heads(target, heads=['ancestor-new'])
689 backend.pull_heads(source, heads=['change-rebased'])
696 backend.pull_heads(source, heads=['change-rebased'])
690
697
691 # update PR
698 # update PR
692 self.app.post(
699 self.app.post(
693 url(controller='pullrequests', action='update',
700 url(controller='pullrequests', action='update',
694 repo_name=target.repo_name,
701 repo_name=target.repo_name,
695 pull_request_id=str(pull_request_id)),
702 pull_request_id=str(pull_request_id)),
696 params={'update_commits': 'true', '_method': 'put',
703 params={'update_commits': 'true', '_method': 'put',
697 'csrf_token': csrf_token},
704 'csrf_token': csrf_token},
698 status=200)
705 status=200)
699
706
700 # check that we have now both revisions
707 # check that we have now both revisions
701 pull_request = PullRequest.get(pull_request_id)
708 pull_request = PullRequest.get(pull_request_id)
702 assert pull_request.revisions == [commit_ids['change-rebased']]
709 assert pull_request.revisions == [commit_ids['change-rebased']]
703 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
710 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
704 branch=backend.default_branch_name,
711 branch=backend.default_branch_name,
705 commit_id=commit_ids['ancestor-new'])
712 commit_id=commit_ids['ancestor-new'])
706
713
707 # TODO: johbo: This should be a test on its own
714 # TODO: johbo: This should be a test on its own
708 response = self.app.get(url(
715 response = self.app.get(url(
709 controller='pullrequests', action='index',
716 controller='pullrequests', action='index',
710 repo_name=target.repo_name))
717 repo_name=target.repo_name))
711 assert response.status_int == 200
718 assert response.status_int == 200
712 assert 'Pull request updated to' in response.body
719 assert 'Pull request updated to' in response.body
713 assert 'with 1 added, 1 removed commits.' in response.body
720 assert 'with 1 added, 1 removed commits.' in response.body
714
721
715 def test_update_of_ancestor_reference(self, backend, csrf_token):
722 def test_update_of_ancestor_reference(self, backend, csrf_token):
716 commits = [
723 commits = [
717 {'message': 'ancestor'},
724 {'message': 'ancestor'},
718 {'message': 'change'},
725 {'message': 'change'},
719 {'message': 'change-2'},
726 {'message': 'change-2'},
720 {'message': 'ancestor-new', 'parents': ['ancestor']},
727 {'message': 'ancestor-new', 'parents': ['ancestor']},
721 {'message': 'change-rebased'},
728 {'message': 'change-rebased'},
722 ]
729 ]
723 commit_ids = backend.create_master_repo(commits)
730 commit_ids = backend.create_master_repo(commits)
724 target = backend.create_repo(heads=['ancestor'])
731 target = backend.create_repo(heads=['ancestor'])
725 source = backend.create_repo(heads=['change'])
732 source = backend.create_repo(heads=['change'])
726
733
727 # create pr from a in source to A in target
734 # create pr from a in source to A in target
728 pull_request = PullRequest()
735 pull_request = PullRequest()
729 pull_request.source_repo = source
736 pull_request.source_repo = source
730 # TODO: johbo: Make sure that we write the source ref this way!
737 # TODO: johbo: Make sure that we write the source ref this way!
731 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
738 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
732 branch=backend.default_branch_name,
739 branch=backend.default_branch_name,
733 commit_id=commit_ids['change'])
740 commit_id=commit_ids['change'])
734 pull_request.target_repo = target
741 pull_request.target_repo = target
735 # TODO: johbo: Target ref should be branch based, since tip can jump
742 # TODO: johbo: Target ref should be branch based, since tip can jump
736 # from branch to branch
743 # from branch to branch
737 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
744 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
738 branch=backend.default_branch_name,
745 branch=backend.default_branch_name,
739 commit_id=commit_ids['ancestor'])
746 commit_id=commit_ids['ancestor'])
740 pull_request.revisions = [commit_ids['change']]
747 pull_request.revisions = [commit_ids['change']]
741 pull_request.title = u"Test"
748 pull_request.title = u"Test"
742 pull_request.description = u"Description"
749 pull_request.description = u"Description"
743 pull_request.author = UserModel().get_by_username(
750 pull_request.author = UserModel().get_by_username(
744 TEST_USER_ADMIN_LOGIN)
751 TEST_USER_ADMIN_LOGIN)
745 Session().add(pull_request)
752 Session().add(pull_request)
746 Session().commit()
753 Session().commit()
747 pull_request_id = pull_request.pull_request_id
754 pull_request_id = pull_request.pull_request_id
748
755
749 # target has ancestor - ancestor-new
756 # target has ancestor - ancestor-new
750 # source has ancestor - ancestor-new - change-rebased
757 # source has ancestor - ancestor-new - change-rebased
751 backend.pull_heads(target, heads=['ancestor-new'])
758 backend.pull_heads(target, heads=['ancestor-new'])
752 backend.pull_heads(source, heads=['change-rebased'])
759 backend.pull_heads(source, heads=['change-rebased'])
753
760
754 # update PR
761 # update PR
755 self.app.post(
762 self.app.post(
756 url(controller='pullrequests', action='update',
763 url(controller='pullrequests', action='update',
757 repo_name=target.repo_name,
764 repo_name=target.repo_name,
758 pull_request_id=str(pull_request_id)),
765 pull_request_id=str(pull_request_id)),
759 params={'update_commits': 'true', '_method': 'put',
766 params={'update_commits': 'true', '_method': 'put',
760 'csrf_token': csrf_token},
767 'csrf_token': csrf_token},
761 status=200)
768 status=200)
762
769
763 # Expect the target reference to be updated correctly
770 # Expect the target reference to be updated correctly
764 pull_request = PullRequest.get(pull_request_id)
771 pull_request = PullRequest.get(pull_request_id)
765 assert pull_request.revisions == [commit_ids['change-rebased']]
772 assert pull_request.revisions == [commit_ids['change-rebased']]
766 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
773 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
767 branch=backend.default_branch_name,
774 branch=backend.default_branch_name,
768 commit_id=commit_ids['ancestor-new'])
775 commit_id=commit_ids['ancestor-new'])
769 assert pull_request.target_ref == expected_target_ref
776 assert pull_request.target_ref == expected_target_ref
770
777
771 def test_remove_pull_request_branch(self, backend_git, csrf_token):
778 def test_remove_pull_request_branch(self, backend_git, csrf_token):
772 branch_name = 'development'
779 branch_name = 'development'
773 commits = [
780 commits = [
774 {'message': 'initial-commit'},
781 {'message': 'initial-commit'},
775 {'message': 'old-feature'},
782 {'message': 'old-feature'},
776 {'message': 'new-feature', 'branch': branch_name},
783 {'message': 'new-feature', 'branch': branch_name},
777 ]
784 ]
778 repo = backend_git.create_repo(commits)
785 repo = backend_git.create_repo(commits)
779 commit_ids = backend_git.commit_ids
786 commit_ids = backend_git.commit_ids
780
787
781 pull_request = PullRequest()
788 pull_request = PullRequest()
782 pull_request.source_repo = repo
789 pull_request.source_repo = repo
783 pull_request.target_repo = repo
790 pull_request.target_repo = repo
784 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
791 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
785 branch=branch_name, commit_id=commit_ids['new-feature'])
792 branch=branch_name, commit_id=commit_ids['new-feature'])
786 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
793 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
787 branch=backend_git.default_branch_name,
794 branch=backend_git.default_branch_name,
788 commit_id=commit_ids['old-feature'])
795 commit_id=commit_ids['old-feature'])
789 pull_request.revisions = [commit_ids['new-feature']]
796 pull_request.revisions = [commit_ids['new-feature']]
790 pull_request.title = u"Test"
797 pull_request.title = u"Test"
791 pull_request.description = u"Description"
798 pull_request.description = u"Description"
792 pull_request.author = UserModel().get_by_username(
799 pull_request.author = UserModel().get_by_username(
793 TEST_USER_ADMIN_LOGIN)
800 TEST_USER_ADMIN_LOGIN)
794 Session().add(pull_request)
801 Session().add(pull_request)
795 Session().commit()
802 Session().commit()
796
803
797 vcs = repo.scm_instance()
804 vcs = repo.scm_instance()
798 vcs.remove_ref('refs/heads/{}'.format(branch_name))
805 vcs.remove_ref('refs/heads/{}'.format(branch_name))
799
806
800 response = self.app.get(url(
807 response = self.app.get(url(
801 controller='pullrequests', action='show',
808 controller='pullrequests', action='show',
802 repo_name=repo.repo_name,
809 repo_name=repo.repo_name,
803 pull_request_id=str(pull_request.pull_request_id)))
810 pull_request_id=str(pull_request.pull_request_id)))
804
811
805 assert response.status_int == 200
812 assert response.status_int == 200
806 assert_response = AssertResponse(response)
813 assert_response = AssertResponse(response)
807 assert_response.element_contains(
814 assert_response.element_contains(
808 '#changeset_compare_view_content .alert strong',
815 '#changeset_compare_view_content .alert strong',
809 'Missing commits')
816 'Missing commits')
810 assert_response.element_contains(
817 assert_response.element_contains(
811 '#changeset_compare_view_content .alert',
818 '#changeset_compare_view_content .alert',
812 'This pull request cannot be displayed, because one or more'
819 'This pull request cannot be displayed, because one or more'
813 ' commits no longer exist in the source repository.')
820 ' commits no longer exist in the source repository.')
814
821
815 def test_strip_commits_from_pull_request(
822 def test_strip_commits_from_pull_request(
816 self, backend, pr_util, csrf_token):
823 self, backend, pr_util, csrf_token):
817 commits = [
824 commits = [
818 {'message': 'initial-commit'},
825 {'message': 'initial-commit'},
819 {'message': 'old-feature'},
826 {'message': 'old-feature'},
820 {'message': 'new-feature', 'parents': ['initial-commit']},
827 {'message': 'new-feature', 'parents': ['initial-commit']},
821 ]
828 ]
822 pull_request = pr_util.create_pull_request(
829 pull_request = pr_util.create_pull_request(
823 commits, target_head='initial-commit', source_head='new-feature',
830 commits, target_head='initial-commit', source_head='new-feature',
824 revisions=['new-feature'])
831 revisions=['new-feature'])
825
832
826 vcs = pr_util.source_repository.scm_instance()
833 vcs = pr_util.source_repository.scm_instance()
827 if backend.alias == 'git':
834 if backend.alias == 'git':
828 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
835 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
829 else:
836 else:
830 vcs.strip(pr_util.commit_ids['new-feature'])
837 vcs.strip(pr_util.commit_ids['new-feature'])
831
838
832 response = self.app.get(url(
839 response = self.app.get(url(
833 controller='pullrequests', action='show',
840 controller='pullrequests', action='show',
834 repo_name=pr_util.target_repository.repo_name,
841 repo_name=pr_util.target_repository.repo_name,
835 pull_request_id=str(pull_request.pull_request_id)))
842 pull_request_id=str(pull_request.pull_request_id)))
836
843
837 assert response.status_int == 200
844 assert response.status_int == 200
838 assert_response = AssertResponse(response)
845 assert_response = AssertResponse(response)
839 assert_response.element_contains(
846 assert_response.element_contains(
840 '#changeset_compare_view_content .alert strong',
847 '#changeset_compare_view_content .alert strong',
841 'Missing commits')
848 'Missing commits')
842 assert_response.element_contains(
849 assert_response.element_contains(
843 '#changeset_compare_view_content .alert',
850 '#changeset_compare_view_content .alert',
844 'This pull request cannot be displayed, because one or more'
851 'This pull request cannot be displayed, because one or more'
845 ' commits no longer exist in the source repository.')
852 ' commits no longer exist in the source repository.')
846 assert_response.element_contains(
853 assert_response.element_contains(
847 '#update_commits',
854 '#update_commits',
848 'Update commits')
855 'Update commits')
849
856
850 def test_strip_commits_and_update(
857 def test_strip_commits_and_update(
851 self, backend, pr_util, csrf_token):
858 self, backend, pr_util, csrf_token):
852 commits = [
859 commits = [
853 {'message': 'initial-commit'},
860 {'message': 'initial-commit'},
854 {'message': 'old-feature'},
861 {'message': 'old-feature'},
855 {'message': 'new-feature', 'parents': ['old-feature']},
862 {'message': 'new-feature', 'parents': ['old-feature']},
856 ]
863 ]
857 pull_request = pr_util.create_pull_request(
864 pull_request = pr_util.create_pull_request(
858 commits, target_head='old-feature', source_head='new-feature',
865 commits, target_head='old-feature', source_head='new-feature',
859 revisions=['new-feature'], mergeable=True)
866 revisions=['new-feature'], mergeable=True)
860
867
861 vcs = pr_util.source_repository.scm_instance()
868 vcs = pr_util.source_repository.scm_instance()
862 if backend.alias == 'git':
869 if backend.alias == 'git':
863 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
870 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
864 else:
871 else:
865 vcs.strip(pr_util.commit_ids['new-feature'])
872 vcs.strip(pr_util.commit_ids['new-feature'])
866
873
867 response = self.app.post(
874 response = self.app.post(
868 url(controller='pullrequests', action='update',
875 url(controller='pullrequests', action='update',
869 repo_name=pull_request.target_repo.repo_name,
876 repo_name=pull_request.target_repo.repo_name,
870 pull_request_id=str(pull_request.pull_request_id)),
877 pull_request_id=str(pull_request.pull_request_id)),
871 params={'update_commits': 'true', '_method': 'put',
878 params={'update_commits': 'true', '_method': 'put',
872 'csrf_token': csrf_token})
879 'csrf_token': csrf_token})
873
880
874 assert response.status_int == 200
881 assert response.status_int == 200
875 assert response.body == 'true'
882 assert response.body == 'true'
876
883
877 # Make sure that after update, it won't raise 500 errors
884 # Make sure that after update, it won't raise 500 errors
878 response = self.app.get(url(
885 response = self.app.get(url(
879 controller='pullrequests', action='show',
886 controller='pullrequests', action='show',
880 repo_name=pr_util.target_repository.repo_name,
887 repo_name=pr_util.target_repository.repo_name,
881 pull_request_id=str(pull_request.pull_request_id)))
888 pull_request_id=str(pull_request.pull_request_id)))
882
889
883 assert response.status_int == 200
890 assert response.status_int == 200
884 assert_response = AssertResponse(response)
891 assert_response = AssertResponse(response)
885 assert_response.element_contains(
892 assert_response.element_contains(
886 '#changeset_compare_view_content .alert strong',
893 '#changeset_compare_view_content .alert strong',
887 'Missing commits')
894 'Missing commits')
888
895
889 def test_branch_is_a_link(self, pr_util):
896 def test_branch_is_a_link(self, pr_util):
890 pull_request = pr_util.create_pull_request()
897 pull_request = pr_util.create_pull_request()
891 pull_request.source_ref = 'branch:origin:1234567890abcdef'
898 pull_request.source_ref = 'branch:origin:1234567890abcdef'
892 pull_request.target_ref = 'branch:target:abcdef1234567890'
899 pull_request.target_ref = 'branch:target:abcdef1234567890'
893 Session().add(pull_request)
900 Session().add(pull_request)
894 Session().commit()
901 Session().commit()
895
902
896 response = self.app.get(url(
903 response = self.app.get(url(
897 controller='pullrequests', action='show',
904 controller='pullrequests', action='show',
898 repo_name=pull_request.target_repo.scm_instance().name,
905 repo_name=pull_request.target_repo.scm_instance().name,
899 pull_request_id=str(pull_request.pull_request_id)))
906 pull_request_id=str(pull_request.pull_request_id)))
900 assert response.status_int == 200
907 assert response.status_int == 200
901 assert_response = AssertResponse(response)
908 assert_response = AssertResponse(response)
902
909
903 origin = assert_response.get_element('.pr-origininfo .tag')
910 origin = assert_response.get_element('.pr-origininfo .tag')
904 origin_children = origin.getchildren()
911 origin_children = origin.getchildren()
905 assert len(origin_children) == 1
912 assert len(origin_children) == 1
906 target = assert_response.get_element('.pr-targetinfo .tag')
913 target = assert_response.get_element('.pr-targetinfo .tag')
907 target_children = target.getchildren()
914 target_children = target.getchildren()
908 assert len(target_children) == 1
915 assert len(target_children) == 1
909
916
910 expected_origin_link = url(
917 expected_origin_link = url(
911 'changelog_home',
918 'changelog_home',
912 repo_name=pull_request.source_repo.scm_instance().name,
919 repo_name=pull_request.source_repo.scm_instance().name,
913 branch='origin')
920 branch='origin')
914 expected_target_link = url(
921 expected_target_link = url(
915 'changelog_home',
922 'changelog_home',
916 repo_name=pull_request.target_repo.scm_instance().name,
923 repo_name=pull_request.target_repo.scm_instance().name,
917 branch='target')
924 branch='target')
918 assert origin_children[0].attrib['href'] == expected_origin_link
925 assert origin_children[0].attrib['href'] == expected_origin_link
919 assert origin_children[0].text == 'branch: origin'
926 assert origin_children[0].text == 'branch: origin'
920 assert target_children[0].attrib['href'] == expected_target_link
927 assert target_children[0].attrib['href'] == expected_target_link
921 assert target_children[0].text == 'branch: target'
928 assert target_children[0].text == 'branch: target'
922
929
923 def test_bookmark_is_not_a_link(self, pr_util):
930 def test_bookmark_is_not_a_link(self, pr_util):
924 pull_request = pr_util.create_pull_request()
931 pull_request = pr_util.create_pull_request()
925 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
932 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
926 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
933 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
927 Session().add(pull_request)
934 Session().add(pull_request)
928 Session().commit()
935 Session().commit()
929
936
930 response = self.app.get(url(
937 response = self.app.get(url(
931 controller='pullrequests', action='show',
938 controller='pullrequests', action='show',
932 repo_name=pull_request.target_repo.scm_instance().name,
939 repo_name=pull_request.target_repo.scm_instance().name,
933 pull_request_id=str(pull_request.pull_request_id)))
940 pull_request_id=str(pull_request.pull_request_id)))
934 assert response.status_int == 200
941 assert response.status_int == 200
935 assert_response = AssertResponse(response)
942 assert_response = AssertResponse(response)
936
943
937 origin = assert_response.get_element('.pr-origininfo .tag')
944 origin = assert_response.get_element('.pr-origininfo .tag')
938 assert origin.text.strip() == 'bookmark: origin'
945 assert origin.text.strip() == 'bookmark: origin'
939 assert origin.getchildren() == []
946 assert origin.getchildren() == []
940
947
941 target = assert_response.get_element('.pr-targetinfo .tag')
948 target = assert_response.get_element('.pr-targetinfo .tag')
942 assert target.text.strip() == 'bookmark: target'
949 assert target.text.strip() == 'bookmark: target'
943 assert target.getchildren() == []
950 assert target.getchildren() == []
944
951
945 def test_tag_is_not_a_link(self, pr_util):
952 def test_tag_is_not_a_link(self, pr_util):
946 pull_request = pr_util.create_pull_request()
953 pull_request = pr_util.create_pull_request()
947 pull_request.source_ref = 'tag:origin:1234567890abcdef'
954 pull_request.source_ref = 'tag:origin:1234567890abcdef'
948 pull_request.target_ref = 'tag:target:abcdef1234567890'
955 pull_request.target_ref = 'tag:target:abcdef1234567890'
949 Session().add(pull_request)
956 Session().add(pull_request)
950 Session().commit()
957 Session().commit()
951
958
952 response = self.app.get(url(
959 response = self.app.get(url(
953 controller='pullrequests', action='show',
960 controller='pullrequests', action='show',
954 repo_name=pull_request.target_repo.scm_instance().name,
961 repo_name=pull_request.target_repo.scm_instance().name,
955 pull_request_id=str(pull_request.pull_request_id)))
962 pull_request_id=str(pull_request.pull_request_id)))
956 assert response.status_int == 200
963 assert response.status_int == 200
957 assert_response = AssertResponse(response)
964 assert_response = AssertResponse(response)
958
965
959 origin = assert_response.get_element('.pr-origininfo .tag')
966 origin = assert_response.get_element('.pr-origininfo .tag')
960 assert origin.text.strip() == 'tag: origin'
967 assert origin.text.strip() == 'tag: origin'
961 assert origin.getchildren() == []
968 assert origin.getchildren() == []
962
969
963 target = assert_response.get_element('.pr-targetinfo .tag')
970 target = assert_response.get_element('.pr-targetinfo .tag')
964 assert target.text.strip() == 'tag: target'
971 assert target.text.strip() == 'tag: target'
965 assert target.getchildren() == []
972 assert target.getchildren() == []
966
973
967 @pytest.mark.parametrize('mergeable', [True, False])
974 @pytest.mark.parametrize('mergeable', [True, False])
968 def test_shadow_repository_link(
975 def test_shadow_repository_link(
969 self, mergeable, pr_util, http_host_only_stub):
976 self, mergeable, pr_util, http_host_only_stub):
970 """
977 """
971 Check that the pull request summary page displays a link to the shadow
978 Check that the pull request summary page displays a link to the shadow
972 repository if the pull request is mergeable. If it is not mergeable
979 repository if the pull request is mergeable. If it is not mergeable
973 the link should not be displayed.
980 the link should not be displayed.
974 """
981 """
975 pull_request = pr_util.create_pull_request(
982 pull_request = pr_util.create_pull_request(
976 mergeable=mergeable, enable_notifications=False)
983 mergeable=mergeable, enable_notifications=False)
977 target_repo = pull_request.target_repo.scm_instance()
984 target_repo = pull_request.target_repo.scm_instance()
978 pr_id = pull_request.pull_request_id
985 pr_id = pull_request.pull_request_id
979 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
986 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
980 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
987 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
981
988
982 response = self.app.get(url(
989 response = self.app.get(url(
983 controller='pullrequests', action='show',
990 controller='pullrequests', action='show',
984 repo_name=target_repo.name,
991 repo_name=target_repo.name,
985 pull_request_id=str(pr_id)))
992 pull_request_id=str(pr_id)))
986
993
987 assertr = AssertResponse(response)
994 assertr = AssertResponse(response)
988 if mergeable:
995 if mergeable:
989 assertr.element_value_contains(
996 assertr.element_value_contains(
990 'div.pr-mergeinfo input', shadow_url)
997 'div.pr-mergeinfo input', shadow_url)
991 assertr.element_value_contains(
998 assertr.element_value_contains(
992 'div.pr-mergeinfo input', 'pr-merge')
999 'div.pr-mergeinfo input', 'pr-merge')
993 else:
1000 else:
994 assertr.no_element_exists('div.pr-mergeinfo')
1001 assertr.no_element_exists('div.pr-mergeinfo')
995
1002
996
1003
997 @pytest.mark.usefixtures('app')
1004 @pytest.mark.usefixtures('app')
998 @pytest.mark.backends("git", "hg")
1005 @pytest.mark.backends("git", "hg")
999 class TestPullrequestsControllerDelete(object):
1006 class TestPullrequestsControllerDelete(object):
1000 def test_pull_request_delete_button_permissions_admin(
1007 def test_pull_request_delete_button_permissions_admin(
1001 self, autologin_user, user_admin, pr_util):
1008 self, autologin_user, user_admin, pr_util):
1002 pull_request = pr_util.create_pull_request(
1009 pull_request = pr_util.create_pull_request(
1003 author=user_admin.username, enable_notifications=False)
1010 author=user_admin.username, enable_notifications=False)
1004
1011
1005 response = self.app.get(url(
1012 response = self.app.get(url(
1006 controller='pullrequests', action='show',
1013 controller='pullrequests', action='show',
1007 repo_name=pull_request.target_repo.scm_instance().name,
1014 repo_name=pull_request.target_repo.scm_instance().name,
1008 pull_request_id=str(pull_request.pull_request_id)))
1015 pull_request_id=str(pull_request.pull_request_id)))
1009
1016
1010 response.mustcontain('id="delete_pullrequest"')
1017 response.mustcontain('id="delete_pullrequest"')
1011 response.mustcontain('Confirm to delete this pull request')
1018 response.mustcontain('Confirm to delete this pull request')
1012
1019
1013 def test_pull_request_delete_button_permissions_owner(
1020 def test_pull_request_delete_button_permissions_owner(
1014 self, autologin_regular_user, user_regular, pr_util):
1021 self, autologin_regular_user, user_regular, pr_util):
1015 pull_request = pr_util.create_pull_request(
1022 pull_request = pr_util.create_pull_request(
1016 author=user_regular.username, enable_notifications=False)
1023 author=user_regular.username, enable_notifications=False)
1017
1024
1018 response = self.app.get(url(
1025 response = self.app.get(url(
1019 controller='pullrequests', action='show',
1026 controller='pullrequests', action='show',
1020 repo_name=pull_request.target_repo.scm_instance().name,
1027 repo_name=pull_request.target_repo.scm_instance().name,
1021 pull_request_id=str(pull_request.pull_request_id)))
1028 pull_request_id=str(pull_request.pull_request_id)))
1022
1029
1023 response.mustcontain('id="delete_pullrequest"')
1030 response.mustcontain('id="delete_pullrequest"')
1024 response.mustcontain('Confirm to delete this pull request')
1031 response.mustcontain('Confirm to delete this pull request')
1025
1032
1026 def test_pull_request_delete_button_permissions_forbidden(
1033 def test_pull_request_delete_button_permissions_forbidden(
1027 self, autologin_regular_user, user_regular, user_admin, pr_util):
1034 self, autologin_regular_user, user_regular, user_admin, pr_util):
1028 pull_request = pr_util.create_pull_request(
1035 pull_request = pr_util.create_pull_request(
1029 author=user_admin.username, enable_notifications=False)
1036 author=user_admin.username, enable_notifications=False)
1030
1037
1031 response = self.app.get(url(
1038 response = self.app.get(url(
1032 controller='pullrequests', action='show',
1039 controller='pullrequests', action='show',
1033 repo_name=pull_request.target_repo.scm_instance().name,
1040 repo_name=pull_request.target_repo.scm_instance().name,
1034 pull_request_id=str(pull_request.pull_request_id)))
1041 pull_request_id=str(pull_request.pull_request_id)))
1035 response.mustcontain(no=['id="delete_pullrequest"'])
1042 response.mustcontain(no=['id="delete_pullrequest"'])
1036 response.mustcontain(no=['Confirm to delete this pull request'])
1043 response.mustcontain(no=['Confirm to delete this pull request'])
1037
1044
1038 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1045 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1039 self, autologin_regular_user, user_regular, user_admin, pr_util,
1046 self, autologin_regular_user, user_regular, user_admin, pr_util,
1040 user_util):
1047 user_util):
1041
1048
1042 pull_request = pr_util.create_pull_request(
1049 pull_request = pr_util.create_pull_request(
1043 author=user_admin.username, enable_notifications=False)
1050 author=user_admin.username, enable_notifications=False)
1044
1051
1045 user_util.grant_user_permission_to_repo(
1052 user_util.grant_user_permission_to_repo(
1046 pull_request.target_repo, user_regular,
1053 pull_request.target_repo, user_regular,
1047 'repository.write')
1054 'repository.write')
1048
1055
1049 response = self.app.get(url(
1056 response = self.app.get(url(
1050 controller='pullrequests', action='show',
1057 controller='pullrequests', action='show',
1051 repo_name=pull_request.target_repo.scm_instance().name,
1058 repo_name=pull_request.target_repo.scm_instance().name,
1052 pull_request_id=str(pull_request.pull_request_id)))
1059 pull_request_id=str(pull_request.pull_request_id)))
1053
1060
1054 response.mustcontain('id="open_edit_pullrequest"')
1061 response.mustcontain('id="open_edit_pullrequest"')
1055 response.mustcontain('id="delete_pullrequest"')
1062 response.mustcontain('id="delete_pullrequest"')
1056 response.mustcontain(no=['Confirm to delete this pull request'])
1063 response.mustcontain(no=['Confirm to delete this pull request'])
1057
1064
1058
1065
1059 def assert_pull_request_status(pull_request, expected_status):
1066 def assert_pull_request_status(pull_request, expected_status):
1060 status = ChangesetStatusModel().calculated_review_status(
1067 status = ChangesetStatusModel().calculated_review_status(
1061 pull_request=pull_request)
1068 pull_request=pull_request)
1062 assert status == expected_status
1069 assert status == expected_status
1063
1070
1064
1071
1065 @pytest.mark.parametrize('action', ['index', 'create'])
1072 @pytest.mark.parametrize('action', ['index', 'create'])
1066 @pytest.mark.usefixtures("autologin_user")
1073 @pytest.mark.usefixtures("autologin_user")
1067 def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action):
1074 def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action):
1068 response = app.get(url(
1075 response = app.get(url(
1069 controller='pullrequests', action=action,
1076 controller='pullrequests', action=action,
1070 repo_name=backend_svn.repo_name))
1077 repo_name=backend_svn.repo_name))
1071 assert response.status_int == 302
1078 assert response.status_int == 302
1072
1079
1073 # Not allowed, redirect to the summary
1080 # Not allowed, redirect to the summary
1074 redirected = response.follow()
1081 redirected = response.follow()
1075 summary_url = h.route_path('repo_summary', repo_name=backend_svn.repo_name)
1082 summary_url = h.route_path('repo_summary', repo_name=backend_svn.repo_name)
1076
1083
1077 # URL adds leading slash and path doesn't have it
1084 # URL adds leading slash and path doesn't have it
1078 assert redirected.request.path == summary_url
1085 assert redirected.request.path == summary_url
1079
1086
1080
1087
1081 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1088 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1082 # TODO: johbo: Global import not possible because models.forms blows up
1089 # TODO: johbo: Global import not possible because models.forms blows up
1083 from rhodecode.controllers.pullrequests import PullrequestsController
1090 from rhodecode.controllers.pullrequests import PullrequestsController
1084 controller = PullrequestsController()
1091 controller = PullrequestsController()
1085 patcher = mock.patch(
1092 patcher = mock.patch(
1086 'rhodecode.model.db.BaseModel.get', return_value=None)
1093 'rhodecode.model.db.BaseModel.get', return_value=None)
1087 with pytest.raises(HTTPNotFound), patcher:
1094 with pytest.raises(HTTPNotFound), patcher:
1088 controller._delete_comment(1)
1095 controller._delete_comment(1)
General Comments 0
You need to be logged in to leave comments. Login now