##// END OF EJS Templates
feat(task-for-automatic-pr-merge): added f-ty to support pull request auto merge for EE. Fixes: RCCE-67
ilin.s -
r5657:027566d5 default
parent child Browse files
Show More
@@ -1,668 +1,669
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib import auth
23 from rhodecode.lib import auth
24 from rhodecode.lib.utils2 import str2bool
24 from rhodecode.lib.utils2 import str2bool
25 from rhodecode.model.db import (
25 from rhodecode.model.db import (
26 Repository, UserRepoToPerm, User)
26 Repository, UserRepoToPerm, User)
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
28 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
29 from rhodecode.model.user import UserModel
29 from rhodecode.model.user import UserModel
30 from rhodecode.tests import (
30 from rhodecode.tests import (
31 login_user_session, logout_user_session,
31 login_user_session, logout_user_session,
32 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
32 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
33 from rhodecode.tests.fixture import Fixture
33 from rhodecode.tests.fixture import Fixture
34 from rhodecode.tests.utils import AssertResponse
34 from rhodecode.tests.utils import AssertResponse
35 from rhodecode.tests.routes import route_path
35 from rhodecode.tests.routes import route_path
36
36
37 fixture = Fixture()
37 fixture = Fixture()
38
38
39
39
40 @pytest.mark.usefixtures("app")
40 @pytest.mark.usefixtures("app")
41 class TestVcsSettings(object):
41 class TestVcsSettings(object):
42 FORM_DATA = {
42 FORM_DATA = {
43 'inherit_global_settings': False,
43 'inherit_global_settings': False,
44 'hooks_changegroup_repo_size': False,
44 'hooks_changegroup_repo_size': False,
45 'hooks_changegroup_push_logger': False,
45 'hooks_changegroup_push_logger': False,
46 'hooks_outgoing_pull_logger': False,
46 'hooks_outgoing_pull_logger': False,
47 'extensions_largefiles': False,
47 'extensions_largefiles': False,
48 'extensions_evolve': False,
48 'extensions_evolve': False,
49 'phases_publish': 'False',
49 'phases_publish': 'False',
50 'rhodecode_pr_merge_enabled': False,
50 'rhodecode_pr_merge_enabled': False,
51 'rhodecode_auto_merge_enabled': False,
51 'rhodecode_use_outdated_comments': False,
52 'rhodecode_use_outdated_comments': False,
52 'new_svn_branch': '',
53 'new_svn_branch': '',
53 'new_svn_tag': ''
54 'new_svn_tag': ''
54 }
55 }
55
56
56 @pytest.mark.skip_backends('svn')
57 @pytest.mark.skip_backends('svn')
57 def test_global_settings_initial_values(self, autologin_user, backend):
58 def test_global_settings_initial_values(self, autologin_user, backend):
58 repo_name = backend.repo_name
59 repo_name = backend.repo_name
59 response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name))
60 response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name))
60
61
61 expected_settings = (
62 expected_settings = (
62 'rhodecode_use_outdated_comments', 'rhodecode_pr_merge_enabled',
63 'rhodecode_use_outdated_comments', 'rhodecode_pr_merge_enabled', 'rhodecode_auto_merge_enabled',
63 'hooks_changegroup_repo_size', 'hooks_changegroup_push_logger',
64 'hooks_changegroup_repo_size', 'hooks_changegroup_push_logger',
64 'hooks_outgoing_pull_logger'
65 'hooks_outgoing_pull_logger'
65 )
66 )
66 for setting in expected_settings:
67 for setting in expected_settings:
67 self.assert_repo_value_equals_global_value(response, setting)
68 self.assert_repo_value_equals_global_value(response, setting)
68
69
69 def test_show_settings_requires_repo_admin_permission(
70 def test_show_settings_requires_repo_admin_permission(
70 self, backend, user_util, settings_util):
71 self, backend, user_util, settings_util):
71 repo = backend.create_repo()
72 repo = backend.create_repo()
72 repo_name = repo.repo_name
73 repo_name = repo.repo_name
73 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
74 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
74 user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
75 user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
75 login_user_session(
76 login_user_session(
76 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
77 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
77 self.app.get(route_path('edit_repo_vcs', repo_name=repo_name), status=200)
78 self.app.get(route_path('edit_repo_vcs', repo_name=repo_name), status=200)
78
79
79 def test_inherit_global_settings_flag_is_true_by_default(
80 def test_inherit_global_settings_flag_is_true_by_default(
80 self, autologin_user, backend):
81 self, autologin_user, backend):
81 repo_name = backend.repo_name
82 repo_name = backend.repo_name
82 response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name))
83 response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name))
83
84
84 assert_response = response.assert_response()
85 assert_response = response.assert_response()
85 element = assert_response.get_element('#inherit_global_settings')
86 element = assert_response.get_element('#inherit_global_settings')
86 assert element.checked
87 assert element.checked
87
88
88 @pytest.mark.parametrize('checked_value', [True, False])
89 @pytest.mark.parametrize('checked_value', [True, False])
89 def test_inherit_global_settings_value(
90 def test_inherit_global_settings_value(
90 self, autologin_user, backend, checked_value, settings_util):
91 self, autologin_user, backend, checked_value, settings_util):
91 repo = backend.create_repo()
92 repo = backend.create_repo()
92 repo_name = repo.repo_name
93 repo_name = repo.repo_name
93 settings_util.create_repo_rhodecode_setting(
94 settings_util.create_repo_rhodecode_setting(
94 repo, 'inherit_vcs_settings', checked_value, 'bool')
95 repo, 'inherit_vcs_settings', checked_value, 'bool')
95 response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name))
96 response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name))
96
97
97 assert_response = response.assert_response()
98 assert_response = response.assert_response()
98 element = assert_response.get_element('#inherit_global_settings')
99 element = assert_response.get_element('#inherit_global_settings')
99 assert element.checked == checked_value
100 assert element.checked == checked_value
100
101
101 @pytest.mark.skip_backends('svn')
102 @pytest.mark.skip_backends('svn')
102 def test_hooks_settings_are_created(
103 def test_hooks_settings_are_created(
103 self, autologin_user, backend, csrf_token):
104 self, autologin_user, backend, csrf_token):
104 repo_name = backend.repo_name
105 repo_name = backend.repo_name
105 data = self.FORM_DATA.copy()
106 data = self.FORM_DATA.copy()
106 data['csrf_token'] = csrf_token
107 data['csrf_token'] = csrf_token
107 self.app.post(
108 self.app.post(
108 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
109 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
109 settings = SettingsModel(repo=repo_name)
110 settings = SettingsModel(repo=repo_name)
110 try:
111 try:
111 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
112 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
112 ui = settings.get_ui_by_section_and_key(section, key)
113 ui = settings.get_ui_by_section_and_key(section, key)
113 assert ui.ui_active is False
114 assert ui.ui_active is False
114 finally:
115 finally:
115 self._cleanup_repo_settings(settings)
116 self._cleanup_repo_settings(settings)
116
117
117 def test_hooks_settings_are_not_created_for_svn(
118 def test_hooks_settings_are_not_created_for_svn(
118 self, autologin_user, backend_svn, csrf_token):
119 self, autologin_user, backend_svn, csrf_token):
119 repo_name = backend_svn.repo_name
120 repo_name = backend_svn.repo_name
120 data = self.FORM_DATA.copy()
121 data = self.FORM_DATA.copy()
121 data['csrf_token'] = csrf_token
122 data['csrf_token'] = csrf_token
122 self.app.post(
123 self.app.post(
123 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
124 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
124 settings = SettingsModel(repo=repo_name)
125 settings = SettingsModel(repo=repo_name)
125 try:
126 try:
126 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
127 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
127 ui = settings.get_ui_by_section_and_key(section, key)
128 ui = settings.get_ui_by_section_and_key(section, key)
128 assert ui is None
129 assert ui is None
129 finally:
130 finally:
130 self._cleanup_repo_settings(settings)
131 self._cleanup_repo_settings(settings)
131
132
132 @pytest.mark.skip_backends('svn')
133 @pytest.mark.skip_backends('svn')
133 def test_hooks_settings_are_updated(
134 def test_hooks_settings_are_updated(
134 self, autologin_user, backend, csrf_token):
135 self, autologin_user, backend, csrf_token):
135 repo_name = backend.repo_name
136 repo_name = backend.repo_name
136 settings = SettingsModel(repo=repo_name)
137 settings = SettingsModel(repo=repo_name)
137 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
138 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
138 settings.create_ui_section_value(section, '', key=key, active=True)
139 settings.create_ui_section_value(section, '', key=key, active=True)
139
140
140 data = self.FORM_DATA.copy()
141 data = self.FORM_DATA.copy()
141 data['csrf_token'] = csrf_token
142 data['csrf_token'] = csrf_token
142 self.app.post(
143 self.app.post(
143 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
144 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
144 try:
145 try:
145 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
146 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
146 ui = settings.get_ui_by_section_and_key(section, key)
147 ui = settings.get_ui_by_section_and_key(section, key)
147 assert ui.ui_active is False
148 assert ui.ui_active is False
148 finally:
149 finally:
149 self._cleanup_repo_settings(settings)
150 self._cleanup_repo_settings(settings)
150
151
151 def test_hooks_settings_are_not_updated_for_svn(
152 def test_hooks_settings_are_not_updated_for_svn(
152 self, autologin_user, backend_svn, csrf_token):
153 self, autologin_user, backend_svn, csrf_token):
153 repo_name = backend_svn.repo_name
154 repo_name = backend_svn.repo_name
154 settings = SettingsModel(repo=repo_name)
155 settings = SettingsModel(repo=repo_name)
155 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
156 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
156 settings.create_ui_section_value(section, '', key=key, active=True)
157 settings.create_ui_section_value(section, '', key=key, active=True)
157
158
158 data = self.FORM_DATA.copy()
159 data = self.FORM_DATA.copy()
159 data['csrf_token'] = csrf_token
160 data['csrf_token'] = csrf_token
160 self.app.post(
161 self.app.post(
161 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
162 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
162 try:
163 try:
163 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
164 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
164 ui = settings.get_ui_by_section_and_key(section, key)
165 ui = settings.get_ui_by_section_and_key(section, key)
165 assert ui.ui_active is True
166 assert ui.ui_active is True
166 finally:
167 finally:
167 self._cleanup_repo_settings(settings)
168 self._cleanup_repo_settings(settings)
168
169
169 @pytest.mark.skip_backends('svn')
170 @pytest.mark.skip_backends('svn')
170 def test_pr_settings_are_created(
171 def test_pr_settings_are_created(
171 self, autologin_user, backend, csrf_token):
172 self, autologin_user, backend, csrf_token):
172 repo_name = backend.repo_name
173 repo_name = backend.repo_name
173 data = self.FORM_DATA.copy()
174 data = self.FORM_DATA.copy()
174 data['csrf_token'] = csrf_token
175 data['csrf_token'] = csrf_token
175 self.app.post(
176 self.app.post(
176 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
177 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
177 settings = SettingsModel(repo=repo_name)
178 settings = SettingsModel(repo=repo_name)
178 try:
179 try:
179 for name in VcsSettingsModel.GENERAL_SETTINGS:
180 for name in VcsSettingsModel.GENERAL_SETTINGS:
180 setting = settings.get_setting_by_name(name)
181 setting = settings.get_setting_by_name(name)
181 assert setting.app_settings_value is False
182 assert setting.app_settings_value is False
182 finally:
183 finally:
183 self._cleanup_repo_settings(settings)
184 self._cleanup_repo_settings(settings)
184
185
185 def test_pr_settings_are_not_created_for_svn(
186 def test_pr_settings_are_not_created_for_svn(
186 self, autologin_user, backend_svn, csrf_token):
187 self, autologin_user, backend_svn, csrf_token):
187 repo_name = backend_svn.repo_name
188 repo_name = backend_svn.repo_name
188 data = self.FORM_DATA.copy()
189 data = self.FORM_DATA.copy()
189 data['csrf_token'] = csrf_token
190 data['csrf_token'] = csrf_token
190 self.app.post(
191 self.app.post(
191 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
192 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
192 settings = SettingsModel(repo=repo_name)
193 settings = SettingsModel(repo=repo_name)
193 try:
194 try:
194 for name in VcsSettingsModel.GENERAL_SETTINGS:
195 for name in VcsSettingsModel.GENERAL_SETTINGS:
195 setting = settings.get_setting_by_name(name)
196 setting = settings.get_setting_by_name(name)
196 assert setting is None
197 assert setting is None
197 finally:
198 finally:
198 self._cleanup_repo_settings(settings)
199 self._cleanup_repo_settings(settings)
199
200
200 def test_pr_settings_creation_requires_repo_admin_permission(
201 def test_pr_settings_creation_requires_repo_admin_permission(
201 self, backend, user_util, settings_util, csrf_token):
202 self, backend, user_util, settings_util, csrf_token):
202 repo = backend.create_repo()
203 repo = backend.create_repo()
203 repo_name = repo.repo_name
204 repo_name = repo.repo_name
204
205
205 logout_user_session(self.app, csrf_token)
206 logout_user_session(self.app, csrf_token)
206 session = login_user_session(
207 session = login_user_session(
207 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
208 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
208 new_csrf_token = auth.get_csrf_token(session)
209 new_csrf_token = auth.get_csrf_token(session)
209
210
210 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
211 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
211 repo = Repository.get_by_repo_name(repo_name)
212 repo = Repository.get_by_repo_name(repo_name)
212 user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
213 user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
213 data = self.FORM_DATA.copy()
214 data = self.FORM_DATA.copy()
214 data['csrf_token'] = new_csrf_token
215 data['csrf_token'] = new_csrf_token
215 settings = SettingsModel(repo=repo_name)
216 settings = SettingsModel(repo=repo_name)
216
217
217 try:
218 try:
218 self.app.post(
219 self.app.post(
219 route_path('edit_repo_vcs_update', repo_name=repo_name), data,
220 route_path('edit_repo_vcs_update', repo_name=repo_name), data,
220 status=302)
221 status=302)
221 finally:
222 finally:
222 self._cleanup_repo_settings(settings)
223 self._cleanup_repo_settings(settings)
223
224
224 @pytest.mark.skip_backends('svn')
225 @pytest.mark.skip_backends('svn')
225 def test_pr_settings_are_updated(
226 def test_pr_settings_are_updated(
226 self, autologin_user, backend, csrf_token):
227 self, autologin_user, backend, csrf_token):
227 repo_name = backend.repo_name
228 repo_name = backend.repo_name
228 settings = SettingsModel(repo=repo_name)
229 settings = SettingsModel(repo=repo_name)
229 for name in VcsSettingsModel.GENERAL_SETTINGS:
230 for name in VcsSettingsModel.GENERAL_SETTINGS:
230 settings.create_or_update_setting(name, True, 'bool')
231 settings.create_or_update_setting(name, True, 'bool')
231
232
232 data = self.FORM_DATA.copy()
233 data = self.FORM_DATA.copy()
233 data['csrf_token'] = csrf_token
234 data['csrf_token'] = csrf_token
234 self.app.post(
235 self.app.post(
235 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
236 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
236 try:
237 try:
237 for name in VcsSettingsModel.GENERAL_SETTINGS:
238 for name in VcsSettingsModel.GENERAL_SETTINGS:
238 setting = settings.get_setting_by_name(name)
239 setting = settings.get_setting_by_name(name)
239 assert setting.app_settings_value is False
240 assert setting.app_settings_value is False
240 finally:
241 finally:
241 self._cleanup_repo_settings(settings)
242 self._cleanup_repo_settings(settings)
242
243
243 def test_pr_settings_are_not_updated_for_svn(
244 def test_pr_settings_are_not_updated_for_svn(
244 self, autologin_user, backend_svn, csrf_token):
245 self, autologin_user, backend_svn, csrf_token):
245 repo_name = backend_svn.repo_name
246 repo_name = backend_svn.repo_name
246 settings = SettingsModel(repo=repo_name)
247 settings = SettingsModel(repo=repo_name)
247 for name in VcsSettingsModel.GENERAL_SETTINGS:
248 for name in VcsSettingsModel.GENERAL_SETTINGS:
248 settings.create_or_update_setting(name, True, 'bool')
249 settings.create_or_update_setting(name, True, 'bool')
249
250
250 data = self.FORM_DATA.copy()
251 data = self.FORM_DATA.copy()
251 data['csrf_token'] = csrf_token
252 data['csrf_token'] = csrf_token
252 self.app.post(
253 self.app.post(
253 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
254 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
254 try:
255 try:
255 for name in VcsSettingsModel.GENERAL_SETTINGS:
256 for name in VcsSettingsModel.GENERAL_SETTINGS:
256 setting = settings.get_setting_by_name(name)
257 setting = settings.get_setting_by_name(name)
257 assert setting.app_settings_value is True
258 assert setting.app_settings_value is True
258 finally:
259 finally:
259 self._cleanup_repo_settings(settings)
260 self._cleanup_repo_settings(settings)
260
261
261 def test_svn_settings_are_created(
262 def test_svn_settings_are_created(
262 self, autologin_user, backend_svn, csrf_token, settings_util):
263 self, autologin_user, backend_svn, csrf_token, settings_util):
263 repo_name = backend_svn.repo_name
264 repo_name = backend_svn.repo_name
264 data = self.FORM_DATA.copy()
265 data = self.FORM_DATA.copy()
265 data['new_svn_tag'] = 'svn-tag'
266 data['new_svn_tag'] = 'svn-tag'
266 data['new_svn_branch'] = 'svn-branch'
267 data['new_svn_branch'] = 'svn-branch'
267 data['csrf_token'] = csrf_token
268 data['csrf_token'] = csrf_token
268
269
269 # Create few global settings to make sure that uniqueness validators
270 # Create few global settings to make sure that uniqueness validators
270 # are not triggered
271 # are not triggered
271 settings_util.create_rhodecode_ui(
272 settings_util.create_rhodecode_ui(
272 VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch')
273 VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch')
273 settings_util.create_rhodecode_ui(
274 settings_util.create_rhodecode_ui(
274 VcsSettingsModel.SVN_TAG_SECTION, 'svn-tag')
275 VcsSettingsModel.SVN_TAG_SECTION, 'svn-tag')
275
276
276 self.app.post(
277 self.app.post(
277 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
278 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
278 settings = SettingsModel(repo=repo_name)
279 settings = SettingsModel(repo=repo_name)
279 try:
280 try:
280 svn_branches = settings.get_ui_by_section(
281 svn_branches = settings.get_ui_by_section(
281 VcsSettingsModel.SVN_BRANCH_SECTION)
282 VcsSettingsModel.SVN_BRANCH_SECTION)
282 svn_branch_names = [b.ui_value for b in svn_branches]
283 svn_branch_names = [b.ui_value for b in svn_branches]
283 svn_tags = settings.get_ui_by_section(
284 svn_tags = settings.get_ui_by_section(
284 VcsSettingsModel.SVN_TAG_SECTION)
285 VcsSettingsModel.SVN_TAG_SECTION)
285 svn_tag_names = [b.ui_value for b in svn_tags]
286 svn_tag_names = [b.ui_value for b in svn_tags]
286 assert 'svn-branch' in svn_branch_names
287 assert 'svn-branch' in svn_branch_names
287 assert 'svn-tag' in svn_tag_names
288 assert 'svn-tag' in svn_tag_names
288 finally:
289 finally:
289 self._cleanup_repo_settings(settings)
290 self._cleanup_repo_settings(settings)
290
291
291 def test_svn_settings_are_unique(
292 def test_svn_settings_are_unique(
292 self, autologin_user, backend_svn, csrf_token, settings_util):
293 self, autologin_user, backend_svn, csrf_token, settings_util):
293 repo = backend_svn.repo
294 repo = backend_svn.repo
294 repo_name = repo.repo_name
295 repo_name = repo.repo_name
295 data = self.FORM_DATA.copy()
296 data = self.FORM_DATA.copy()
296 data['new_svn_tag'] = 'test_tag'
297 data['new_svn_tag'] = 'test_tag'
297 data['new_svn_branch'] = 'test_branch'
298 data['new_svn_branch'] = 'test_branch'
298 data['csrf_token'] = csrf_token
299 data['csrf_token'] = csrf_token
299 settings_util.create_repo_rhodecode_ui(
300 settings_util.create_repo_rhodecode_ui(
300 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch')
301 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch')
301 settings_util.create_repo_rhodecode_ui(
302 settings_util.create_repo_rhodecode_ui(
302 repo, VcsSettingsModel.SVN_TAG_SECTION, 'test_tag')
303 repo, VcsSettingsModel.SVN_TAG_SECTION, 'test_tag')
303
304
304 response = self.app.post(
305 response = self.app.post(
305 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=200)
306 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=200)
306 response.mustcontain('Pattern already exists')
307 response.mustcontain('Pattern already exists')
307
308
308 def test_svn_settings_with_empty_values_are_not_created(
309 def test_svn_settings_with_empty_values_are_not_created(
309 self, autologin_user, backend_svn, csrf_token):
310 self, autologin_user, backend_svn, csrf_token):
310 repo_name = backend_svn.repo_name
311 repo_name = backend_svn.repo_name
311 data = self.FORM_DATA.copy()
312 data = self.FORM_DATA.copy()
312 data['csrf_token'] = csrf_token
313 data['csrf_token'] = csrf_token
313 self.app.post(
314 self.app.post(
314 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
315 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
315 settings = SettingsModel(repo=repo_name)
316 settings = SettingsModel(repo=repo_name)
316 try:
317 try:
317 svn_branches = settings.get_ui_by_section(
318 svn_branches = settings.get_ui_by_section(
318 VcsSettingsModel.SVN_BRANCH_SECTION)
319 VcsSettingsModel.SVN_BRANCH_SECTION)
319 svn_tags = settings.get_ui_by_section(
320 svn_tags = settings.get_ui_by_section(
320 VcsSettingsModel.SVN_TAG_SECTION)
321 VcsSettingsModel.SVN_TAG_SECTION)
321 assert len(svn_branches) == 0
322 assert len(svn_branches) == 0
322 assert len(svn_tags) == 0
323 assert len(svn_tags) == 0
323 finally:
324 finally:
324 self._cleanup_repo_settings(settings)
325 self._cleanup_repo_settings(settings)
325
326
326 def test_svn_settings_are_shown_for_svn_repository(
327 def test_svn_settings_are_shown_for_svn_repository(
327 self, autologin_user, backend_svn, csrf_token):
328 self, autologin_user, backend_svn, csrf_token):
328 repo_name = backend_svn.repo_name
329 repo_name = backend_svn.repo_name
329 response = self.app.get(
330 response = self.app.get(
330 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
331 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
331 response.mustcontain('Subversion Settings')
332 response.mustcontain('Subversion Settings')
332
333
333 @pytest.mark.skip_backends('svn')
334 @pytest.mark.skip_backends('svn')
334 def test_svn_settings_are_not_created_for_not_svn_repository(
335 def test_svn_settings_are_not_created_for_not_svn_repository(
335 self, autologin_user, backend, csrf_token):
336 self, autologin_user, backend, csrf_token):
336 repo_name = backend.repo_name
337 repo_name = backend.repo_name
337 data = self.FORM_DATA.copy()
338 data = self.FORM_DATA.copy()
338 data['csrf_token'] = csrf_token
339 data['csrf_token'] = csrf_token
339 self.app.post(
340 self.app.post(
340 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
341 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
341 settings = SettingsModel(repo=repo_name)
342 settings = SettingsModel(repo=repo_name)
342 try:
343 try:
343 svn_branches = settings.get_ui_by_section(
344 svn_branches = settings.get_ui_by_section(
344 VcsSettingsModel.SVN_BRANCH_SECTION)
345 VcsSettingsModel.SVN_BRANCH_SECTION)
345 svn_tags = settings.get_ui_by_section(
346 svn_tags = settings.get_ui_by_section(
346 VcsSettingsModel.SVN_TAG_SECTION)
347 VcsSettingsModel.SVN_TAG_SECTION)
347 assert len(svn_branches) == 0
348 assert len(svn_branches) == 0
348 assert len(svn_tags) == 0
349 assert len(svn_tags) == 0
349 finally:
350 finally:
350 self._cleanup_repo_settings(settings)
351 self._cleanup_repo_settings(settings)
351
352
352 @pytest.mark.skip_backends('svn')
353 @pytest.mark.skip_backends('svn')
353 def test_svn_settings_are_shown_only_for_svn_repository(
354 def test_svn_settings_are_shown_only_for_svn_repository(
354 self, autologin_user, backend, csrf_token):
355 self, autologin_user, backend, csrf_token):
355 repo_name = backend.repo_name
356 repo_name = backend.repo_name
356 response = self.app.get(
357 response = self.app.get(
357 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
358 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
358 response.mustcontain(no='Subversion Settings')
359 response.mustcontain(no='Subversion Settings')
359
360
360 def test_hg_settings_are_created(
361 def test_hg_settings_are_created(
361 self, autologin_user, backend_hg, csrf_token):
362 self, autologin_user, backend_hg, csrf_token):
362 repo_name = backend_hg.repo_name
363 repo_name = backend_hg.repo_name
363 data = self.FORM_DATA.copy()
364 data = self.FORM_DATA.copy()
364 data['new_svn_tag'] = 'svn-tag'
365 data['new_svn_tag'] = 'svn-tag'
365 data['new_svn_branch'] = 'svn-branch'
366 data['new_svn_branch'] = 'svn-branch'
366 data['csrf_token'] = csrf_token
367 data['csrf_token'] = csrf_token
367 self.app.post(
368 self.app.post(
368 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
369 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
369 settings = SettingsModel(repo=repo_name)
370 settings = SettingsModel(repo=repo_name)
370 try:
371 try:
371 largefiles_ui = settings.get_ui_by_section_and_key(
372 largefiles_ui = settings.get_ui_by_section_and_key(
372 'extensions', 'largefiles')
373 'extensions', 'largefiles')
373 assert largefiles_ui.ui_active is False
374 assert largefiles_ui.ui_active is False
374 phases_ui = settings.get_ui_by_section_and_key(
375 phases_ui = settings.get_ui_by_section_and_key(
375 'phases', 'publish')
376 'phases', 'publish')
376 assert str2bool(phases_ui.ui_value) is False
377 assert str2bool(phases_ui.ui_value) is False
377 finally:
378 finally:
378 self._cleanup_repo_settings(settings)
379 self._cleanup_repo_settings(settings)
379
380
380 def test_hg_settings_are_updated(
381 def test_hg_settings_are_updated(
381 self, autologin_user, backend_hg, csrf_token):
382 self, autologin_user, backend_hg, csrf_token):
382 repo_name = backend_hg.repo_name
383 repo_name = backend_hg.repo_name
383 settings = SettingsModel(repo=repo_name)
384 settings = SettingsModel(repo=repo_name)
384 settings.create_ui_section_value(
385 settings.create_ui_section_value(
385 'extensions', '', key='largefiles', active=True)
386 'extensions', '', key='largefiles', active=True)
386 settings.create_ui_section_value(
387 settings.create_ui_section_value(
387 'phases', '1', key='publish', active=True)
388 'phases', '1', key='publish', active=True)
388
389
389 data = self.FORM_DATA.copy()
390 data = self.FORM_DATA.copy()
390 data['csrf_token'] = csrf_token
391 data['csrf_token'] = csrf_token
391 self.app.post(
392 self.app.post(
392 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
393 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
393 try:
394 try:
394 largefiles_ui = settings.get_ui_by_section_and_key(
395 largefiles_ui = settings.get_ui_by_section_and_key(
395 'extensions', 'largefiles')
396 'extensions', 'largefiles')
396 assert largefiles_ui.ui_active is False
397 assert largefiles_ui.ui_active is False
397 phases_ui = settings.get_ui_by_section_and_key(
398 phases_ui = settings.get_ui_by_section_and_key(
398 'phases', 'publish')
399 'phases', 'publish')
399 assert str2bool(phases_ui.ui_value) is False
400 assert str2bool(phases_ui.ui_value) is False
400 finally:
401 finally:
401 self._cleanup_repo_settings(settings)
402 self._cleanup_repo_settings(settings)
402
403
403 def test_hg_settings_are_shown_for_hg_repository(
404 def test_hg_settings_are_shown_for_hg_repository(
404 self, autologin_user, backend_hg, csrf_token):
405 self, autologin_user, backend_hg, csrf_token):
405 repo_name = backend_hg.repo_name
406 repo_name = backend_hg.repo_name
406 response = self.app.get(
407 response = self.app.get(
407 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
408 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
408 response.mustcontain('Mercurial Settings')
409 response.mustcontain('Mercurial Settings')
409
410
410 @pytest.mark.skip_backends('hg')
411 @pytest.mark.skip_backends('hg')
411 def test_hg_settings_are_created_only_for_hg_repository(
412 def test_hg_settings_are_created_only_for_hg_repository(
412 self, autologin_user, backend, csrf_token):
413 self, autologin_user, backend, csrf_token):
413 repo_name = backend.repo_name
414 repo_name = backend.repo_name
414 data = self.FORM_DATA.copy()
415 data = self.FORM_DATA.copy()
415 data['csrf_token'] = csrf_token
416 data['csrf_token'] = csrf_token
416 self.app.post(
417 self.app.post(
417 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
418 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
418 settings = SettingsModel(repo=repo_name)
419 settings = SettingsModel(repo=repo_name)
419 try:
420 try:
420 largefiles_ui = settings.get_ui_by_section_and_key(
421 largefiles_ui = settings.get_ui_by_section_and_key(
421 'extensions', 'largefiles')
422 'extensions', 'largefiles')
422 assert largefiles_ui is None
423 assert largefiles_ui is None
423 phases_ui = settings.get_ui_by_section_and_key(
424 phases_ui = settings.get_ui_by_section_and_key(
424 'phases', 'publish')
425 'phases', 'publish')
425 assert phases_ui is None
426 assert phases_ui is None
426 finally:
427 finally:
427 self._cleanup_repo_settings(settings)
428 self._cleanup_repo_settings(settings)
428
429
429 @pytest.mark.skip_backends('hg')
430 @pytest.mark.skip_backends('hg')
430 def test_hg_settings_are_shown_only_for_hg_repository(
431 def test_hg_settings_are_shown_only_for_hg_repository(
431 self, autologin_user, backend, csrf_token):
432 self, autologin_user, backend, csrf_token):
432 repo_name = backend.repo_name
433 repo_name = backend.repo_name
433 response = self.app.get(
434 response = self.app.get(
434 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
435 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
435 response.mustcontain(no='Mercurial Settings')
436 response.mustcontain(no='Mercurial Settings')
436
437
437 @pytest.mark.skip_backends('hg')
438 @pytest.mark.skip_backends('hg')
438 def test_hg_settings_are_updated_only_for_hg_repository(
439 def test_hg_settings_are_updated_only_for_hg_repository(
439 self, autologin_user, backend, csrf_token):
440 self, autologin_user, backend, csrf_token):
440 repo_name = backend.repo_name
441 repo_name = backend.repo_name
441 settings = SettingsModel(repo=repo_name)
442 settings = SettingsModel(repo=repo_name)
442 settings.create_ui_section_value(
443 settings.create_ui_section_value(
443 'extensions', '', key='largefiles', active=True)
444 'extensions', '', key='largefiles', active=True)
444 settings.create_ui_section_value(
445 settings.create_ui_section_value(
445 'phases', '1', key='publish', active=True)
446 'phases', '1', key='publish', active=True)
446
447
447 data = self.FORM_DATA.copy()
448 data = self.FORM_DATA.copy()
448 data['csrf_token'] = csrf_token
449 data['csrf_token'] = csrf_token
449 self.app.post(
450 self.app.post(
450 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
451 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
451 try:
452 try:
452 largefiles_ui = settings.get_ui_by_section_and_key(
453 largefiles_ui = settings.get_ui_by_section_and_key(
453 'extensions', 'largefiles')
454 'extensions', 'largefiles')
454 assert largefiles_ui.ui_active is True
455 assert largefiles_ui.ui_active is True
455 phases_ui = settings.get_ui_by_section_and_key(
456 phases_ui = settings.get_ui_by_section_and_key(
456 'phases', 'publish')
457 'phases', 'publish')
457 assert phases_ui.ui_value == '1'
458 assert phases_ui.ui_value == '1'
458 finally:
459 finally:
459 self._cleanup_repo_settings(settings)
460 self._cleanup_repo_settings(settings)
460
461
461 def test_per_repo_svn_settings_are_displayed(
462 def test_per_repo_svn_settings_are_displayed(
462 self, autologin_user, backend_svn, settings_util):
463 self, autologin_user, backend_svn, settings_util):
463 repo = backend_svn.create_repo()
464 repo = backend_svn.create_repo()
464 repo_name = repo.repo_name
465 repo_name = repo.repo_name
465 branches = [
466 branches = [
466 settings_util.create_repo_rhodecode_ui(
467 settings_util.create_repo_rhodecode_ui(
467 repo, VcsSettingsModel.SVN_BRANCH_SECTION,
468 repo, VcsSettingsModel.SVN_BRANCH_SECTION,
468 'branch_{}'.format(i))
469 'branch_{}'.format(i))
469 for i in range(10)]
470 for i in range(10)]
470 tags = [
471 tags = [
471 settings_util.create_repo_rhodecode_ui(
472 settings_util.create_repo_rhodecode_ui(
472 repo, VcsSettingsModel.SVN_TAG_SECTION, 'tag_{}'.format(i))
473 repo, VcsSettingsModel.SVN_TAG_SECTION, 'tag_{}'.format(i))
473 for i in range(10)]
474 for i in range(10)]
474
475
475 response = self.app.get(
476 response = self.app.get(
476 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
477 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
477 assert_response = response.assert_response()
478 assert_response = response.assert_response()
478 for branch in branches:
479 for branch in branches:
479 css_selector = '[name=branch_value_{}]'.format(branch.ui_id)
480 css_selector = '[name=branch_value_{}]'.format(branch.ui_id)
480 element = assert_response.get_element(css_selector)
481 element = assert_response.get_element(css_selector)
481 assert element.value == branch.ui_value
482 assert element.value == branch.ui_value
482 for tag in tags:
483 for tag in tags:
483 css_selector = '[name=tag_ui_value_new_{}]'.format(tag.ui_id)
484 css_selector = '[name=tag_ui_value_new_{}]'.format(tag.ui_id)
484 element = assert_response.get_element(css_selector)
485 element = assert_response.get_element(css_selector)
485 assert element.value == tag.ui_value
486 assert element.value == tag.ui_value
486
487
487 def test_per_repo_hg_and_pr_settings_are_not_displayed_for_svn(
488 def test_per_repo_hg_and_pr_settings_are_not_displayed_for_svn(
488 self, autologin_user, backend_svn, settings_util):
489 self, autologin_user, backend_svn, settings_util):
489 repo = backend_svn.create_repo()
490 repo = backend_svn.create_repo()
490 repo_name = repo.repo_name
491 repo_name = repo.repo_name
491 response = self.app.get(
492 response = self.app.get(
492 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
493 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
493 response.mustcontain(no='<label>Hooks:</label>')
494 response.mustcontain(no='<label>Hooks:</label>')
494 response.mustcontain(no='<label>Pull Request Settings:</label>')
495 response.mustcontain(no='<label>Pull Request Settings:</label>')
495
496
496 def test_inherit_global_settings_value_is_saved(
497 def test_inherit_global_settings_value_is_saved(
497 self, autologin_user, backend, csrf_token):
498 self, autologin_user, backend, csrf_token):
498 repo_name = backend.repo_name
499 repo_name = backend.repo_name
499 data = self.FORM_DATA.copy()
500 data = self.FORM_DATA.copy()
500 data['csrf_token'] = csrf_token
501 data['csrf_token'] = csrf_token
501 data['inherit_global_settings'] = True
502 data['inherit_global_settings'] = True
502 self.app.post(
503 self.app.post(
503 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
504 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
504
505
505 settings = SettingsModel(repo=repo_name)
506 settings = SettingsModel(repo=repo_name)
506 vcs_settings = VcsSettingsModel(repo=repo_name)
507 vcs_settings = VcsSettingsModel(repo=repo_name)
507 try:
508 try:
508 assert vcs_settings.inherit_global_settings is True
509 assert vcs_settings.inherit_global_settings is True
509 finally:
510 finally:
510 self._cleanup_repo_settings(settings)
511 self._cleanup_repo_settings(settings)
511
512
512 def test_repo_cache_is_invalidated_when_settings_are_updated(
513 def test_repo_cache_is_invalidated_when_settings_are_updated(
513 self, autologin_user, backend, csrf_token):
514 self, autologin_user, backend, csrf_token):
514 repo_name = backend.repo_name
515 repo_name = backend.repo_name
515 data = self.FORM_DATA.copy()
516 data = self.FORM_DATA.copy()
516 data['csrf_token'] = csrf_token
517 data['csrf_token'] = csrf_token
517 data['inherit_global_settings'] = True
518 data['inherit_global_settings'] = True
518 settings = SettingsModel(repo=repo_name)
519 settings = SettingsModel(repo=repo_name)
519
520
520 invalidation_patcher = mock.patch(
521 invalidation_patcher = mock.patch(
521 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
522 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
522 with invalidation_patcher as invalidation_mock:
523 with invalidation_patcher as invalidation_mock:
523 self.app.post(
524 self.app.post(
524 route_path('edit_repo_vcs_update', repo_name=repo_name), data,
525 route_path('edit_repo_vcs_update', repo_name=repo_name), data,
525 status=302)
526 status=302)
526 try:
527 try:
527 invalidation_mock.assert_called_once_with(repo_name, delete=True)
528 invalidation_mock.assert_called_once_with(repo_name, delete=True)
528 finally:
529 finally:
529 self._cleanup_repo_settings(settings)
530 self._cleanup_repo_settings(settings)
530
531
531 def test_other_settings_not_saved_inherit_global_settings_is_true(
532 def test_other_settings_not_saved_inherit_global_settings_is_true(
532 self, autologin_user, backend, csrf_token):
533 self, autologin_user, backend, csrf_token):
533 repo_name = backend.repo_name
534 repo_name = backend.repo_name
534 data = self.FORM_DATA.copy()
535 data = self.FORM_DATA.copy()
535 data['csrf_token'] = csrf_token
536 data['csrf_token'] = csrf_token
536 data['inherit_global_settings'] = True
537 data['inherit_global_settings'] = True
537 self.app.post(
538 self.app.post(
538 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
539 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
539
540
540 settings = SettingsModel(repo=repo_name)
541 settings = SettingsModel(repo=repo_name)
541 ui_settings = (
542 ui_settings = (
542 VcsSettingsModel.HOOKS_SETTINGS + VcsSettingsModel.HG_SETTINGS)
543 VcsSettingsModel.HOOKS_SETTINGS + VcsSettingsModel.HG_SETTINGS)
543
544
544 vcs_settings = []
545 vcs_settings = []
545 try:
546 try:
546 for section, key in ui_settings:
547 for section, key in ui_settings:
547 ui = settings.get_ui_by_section_and_key(section, key)
548 ui = settings.get_ui_by_section_and_key(section, key)
548 if ui:
549 if ui:
549 vcs_settings.append(ui)
550 vcs_settings.append(ui)
550 vcs_settings.extend(settings.get_ui_by_section(
551 vcs_settings.extend(settings.get_ui_by_section(
551 VcsSettingsModel.SVN_BRANCH_SECTION))
552 VcsSettingsModel.SVN_BRANCH_SECTION))
552 vcs_settings.extend(settings.get_ui_by_section(
553 vcs_settings.extend(settings.get_ui_by_section(
553 VcsSettingsModel.SVN_TAG_SECTION))
554 VcsSettingsModel.SVN_TAG_SECTION))
554 for name in VcsSettingsModel.GENERAL_SETTINGS:
555 for name in VcsSettingsModel.GENERAL_SETTINGS:
555 setting = settings.get_setting_by_name(name)
556 setting = settings.get_setting_by_name(name)
556 if setting:
557 if setting:
557 vcs_settings.append(setting)
558 vcs_settings.append(setting)
558 assert vcs_settings == []
559 assert vcs_settings == []
559 finally:
560 finally:
560 self._cleanup_repo_settings(settings)
561 self._cleanup_repo_settings(settings)
561
562
562 def test_delete_svn_branch_and_tag_patterns(
563 def test_delete_svn_branch_and_tag_patterns(
563 self, autologin_user, backend_svn, settings_util, csrf_token, xhr_header):
564 self, autologin_user, backend_svn, settings_util, csrf_token, xhr_header):
564 repo = backend_svn.create_repo()
565 repo = backend_svn.create_repo()
565 repo_name = repo.repo_name
566 repo_name = repo.repo_name
566 branch = settings_util.create_repo_rhodecode_ui(
567 branch = settings_util.create_repo_rhodecode_ui(
567 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch',
568 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch',
568 cleanup=False)
569 cleanup=False)
569 tag = settings_util.create_repo_rhodecode_ui(
570 tag = settings_util.create_repo_rhodecode_ui(
570 repo, VcsSettingsModel.SVN_TAG_SECTION, 'test_tag', cleanup=False)
571 repo, VcsSettingsModel.SVN_TAG_SECTION, 'test_tag', cleanup=False)
571 data = {
572 data = {
572 'csrf_token': csrf_token
573 'csrf_token': csrf_token
573 }
574 }
574 for id_ in (branch.ui_id, tag.ui_id):
575 for id_ in (branch.ui_id, tag.ui_id):
575 data['delete_svn_pattern'] = id_,
576 data['delete_svn_pattern'] = id_,
576 self.app.post(
577 self.app.post(
577 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
578 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
578 data, extra_environ=xhr_header, status=200)
579 data, extra_environ=xhr_header, status=200)
579 settings = VcsSettingsModel(repo=repo_name)
580 settings = VcsSettingsModel(repo=repo_name)
580 assert settings.get_repo_svn_branch_patterns() == []
581 assert settings.get_repo_svn_branch_patterns() == []
581
582
582 def test_delete_svn_branch_requires_repo_admin_permission(
583 def test_delete_svn_branch_requires_repo_admin_permission(
583 self, backend_svn, user_util, settings_util, csrf_token, xhr_header):
584 self, backend_svn, user_util, settings_util, csrf_token, xhr_header):
584 repo = backend_svn.create_repo()
585 repo = backend_svn.create_repo()
585 repo_name = repo.repo_name
586 repo_name = repo.repo_name
586
587
587 logout_user_session(self.app, csrf_token)
588 logout_user_session(self.app, csrf_token)
588 session = login_user_session(
589 session = login_user_session(
589 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
590 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
590 csrf_token = auth.get_csrf_token(session)
591 csrf_token = auth.get_csrf_token(session)
591
592
592 repo = Repository.get_by_repo_name(repo_name)
593 repo = Repository.get_by_repo_name(repo_name)
593 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
594 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
594 user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
595 user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
595 branch = settings_util.create_repo_rhodecode_ui(
596 branch = settings_util.create_repo_rhodecode_ui(
596 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch',
597 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch',
597 cleanup=False)
598 cleanup=False)
598 data = {
599 data = {
599 'csrf_token': csrf_token,
600 'csrf_token': csrf_token,
600 'delete_svn_pattern': branch.ui_id
601 'delete_svn_pattern': branch.ui_id
601 }
602 }
602 self.app.post(
603 self.app.post(
603 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
604 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
604 data, extra_environ=xhr_header, status=200)
605 data, extra_environ=xhr_header, status=200)
605
606
606 def test_delete_svn_branch_raises_400_when_not_found(
607 def test_delete_svn_branch_raises_400_when_not_found(
607 self, autologin_user, backend_svn, settings_util, csrf_token, xhr_header):
608 self, autologin_user, backend_svn, settings_util, csrf_token, xhr_header):
608 repo_name = backend_svn.repo_name
609 repo_name = backend_svn.repo_name
609 data = {
610 data = {
610 'delete_svn_pattern': 123,
611 'delete_svn_pattern': 123,
611 'csrf_token': csrf_token
612 'csrf_token': csrf_token
612 }
613 }
613 self.app.post(
614 self.app.post(
614 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
615 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
615 data, extra_environ=xhr_header, status=400)
616 data, extra_environ=xhr_header, status=400)
616
617
617 def test_delete_svn_branch_raises_400_when_no_id_specified(
618 def test_delete_svn_branch_raises_400_when_no_id_specified(
618 self, autologin_user, backend_svn, settings_util, csrf_token, xhr_header):
619 self, autologin_user, backend_svn, settings_util, csrf_token, xhr_header):
619 repo_name = backend_svn.repo_name
620 repo_name = backend_svn.repo_name
620 data = {
621 data = {
621 'csrf_token': csrf_token
622 'csrf_token': csrf_token
622 }
623 }
623 self.app.post(
624 self.app.post(
624 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
625 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
625 data, extra_environ=xhr_header, status=400)
626 data, extra_environ=xhr_header, status=400)
626
627
627 def _cleanup_repo_settings(self, settings_model):
628 def _cleanup_repo_settings(self, settings_model):
628 cleanup = []
629 cleanup = []
629 ui_settings = (
630 ui_settings = (
630 VcsSettingsModel.HOOKS_SETTINGS + VcsSettingsModel.HG_SETTINGS)
631 VcsSettingsModel.HOOKS_SETTINGS + VcsSettingsModel.HG_SETTINGS)
631
632
632 for section, key in ui_settings:
633 for section, key in ui_settings:
633 ui = settings_model.get_ui_by_section_and_key(section, key)
634 ui = settings_model.get_ui_by_section_and_key(section, key)
634 if ui:
635 if ui:
635 cleanup.append(ui)
636 cleanup.append(ui)
636
637
637 cleanup.extend(settings_model.get_ui_by_section(
638 cleanup.extend(settings_model.get_ui_by_section(
638 VcsSettingsModel.INHERIT_SETTINGS))
639 VcsSettingsModel.INHERIT_SETTINGS))
639 cleanup.extend(settings_model.get_ui_by_section(
640 cleanup.extend(settings_model.get_ui_by_section(
640 VcsSettingsModel.SVN_BRANCH_SECTION))
641 VcsSettingsModel.SVN_BRANCH_SECTION))
641 cleanup.extend(settings_model.get_ui_by_section(
642 cleanup.extend(settings_model.get_ui_by_section(
642 VcsSettingsModel.SVN_TAG_SECTION))
643 VcsSettingsModel.SVN_TAG_SECTION))
643
644
644 for name in VcsSettingsModel.GENERAL_SETTINGS:
645 for name in VcsSettingsModel.GENERAL_SETTINGS:
645 setting = settings_model.get_setting_by_name(name)
646 setting = settings_model.get_setting_by_name(name)
646 if setting:
647 if setting:
647 cleanup.append(setting)
648 cleanup.append(setting)
648
649
649 for object_ in cleanup:
650 for object_ in cleanup:
650 Session().delete(object_)
651 Session().delete(object_)
651 Session().commit()
652 Session().commit()
652
653
653 def assert_repo_value_equals_global_value(self, response, setting):
654 def assert_repo_value_equals_global_value(self, response, setting):
654 assert_response = response.assert_response()
655 assert_response = response.assert_response()
655 global_css_selector = '[name={}_inherited]'.format(setting)
656 global_css_selector = '[name={}_inherited]'.format(setting)
656 repo_css_selector = '[name={}]'.format(setting)
657 repo_css_selector = '[name={}]'.format(setting)
657 repo_element = assert_response.get_element(repo_css_selector)
658 repo_element = assert_response.get_element(repo_css_selector)
658 global_element = assert_response.get_element(global_css_selector)
659 global_element = assert_response.get_element(global_css_selector)
659 assert repo_element.value == global_element.value
660 assert repo_element.value == global_element.value
660
661
661
662
662 def _get_permission_for_user(user, repo):
663 def _get_permission_for_user(user, repo):
663 perm = UserRepoToPerm.query()\
664 perm = UserRepoToPerm.query()\
664 .filter(UserRepoToPerm.repository ==
665 .filter(UserRepoToPerm.repository ==
665 Repository.get_by_repo_name(repo))\
666 Repository.get_by_repo_name(repo))\
666 .filter(UserRepoToPerm.user == User.get_by_username(user))\
667 .filter(UserRepoToPerm.user == User.get_by_username(user))\
667 .all()
668 .all()
668 return perm
669 return perm
@@ -1,471 +1,472
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 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 import os
19 import os
20 import sys
20 import sys
21 import collections
21 import collections
22
22
23 import time
23 import time
24 import logging.config
24 import logging.config
25
25
26 from paste.gzipper import make_gzip_middleware
26 from paste.gzipper import make_gzip_middleware
27 import pyramid.events
27 import pyramid.events
28 from pyramid.wsgi import wsgiapp
28 from pyramid.wsgi import wsgiapp
29 from pyramid.config import Configurator
29 from pyramid.config import Configurator
30 from pyramid.settings import asbool, aslist
30 from pyramid.settings import asbool, aslist
31 from pyramid.httpexceptions import (
31 from pyramid.httpexceptions import (
32 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
32 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
33 from pyramid.renderers import render_to_response
33 from pyramid.renderers import render_to_response
34
34
35 from rhodecode.model import meta
35 from rhodecode.model import meta
36 from rhodecode.config import patches
36 from rhodecode.config import patches
37
37
38 from rhodecode.config.environment import load_pyramid_environment, propagate_rhodecode_config
38 from rhodecode.config.environment import load_pyramid_environment, propagate_rhodecode_config
39
39
40 import rhodecode.events
40 import rhodecode.events
41 from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
41 from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
42 from rhodecode.lib.middleware.vcs import VCSMiddleware
42 from rhodecode.lib.middleware.vcs import VCSMiddleware
43 from rhodecode.lib.request import Request
43 from rhodecode.lib.request import Request
44 from rhodecode.lib.vcs import VCSCommunicationError
44 from rhodecode.lib.vcs import VCSCommunicationError
45 from rhodecode.lib.exceptions import VCSServerUnavailable
45 from rhodecode.lib.exceptions import VCSServerUnavailable
46 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
46 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
48 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
49 from rhodecode.lib.utils2 import AttributeDict
49 from rhodecode.lib.utils2 import AttributeDict
50 from rhodecode.lib.exc_tracking import store_exception, format_exc
50 from rhodecode.lib.exc_tracking import store_exception, format_exc
51 from rhodecode.subscribers import (
51 from rhodecode.subscribers import (
52 scan_repositories_if_enabled, write_js_routes_if_enabled,
52 auto_merge_pr_if_needed, scan_repositories_if_enabled, write_js_routes_if_enabled,
53 write_metadata_if_needed, write_usage_data, import_license_if_present)
53 write_metadata_if_needed, write_usage_data, import_license_if_present)
54 from rhodecode.lib.statsd_client import StatsdClient
54 from rhodecode.lib.statsd_client import StatsdClient
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 def is_http_error(response):
59 def is_http_error(response):
60 # error which should have traceback
60 # error which should have traceback
61 return response.status_code > 499
61 return response.status_code > 499
62
62
63
63
64 def should_load_all():
64 def should_load_all():
65 """
65 """
66 Returns if all application components should be loaded. In some cases it's
66 Returns if all application components should be loaded. In some cases it's
67 desired to skip apps loading for faster shell script execution
67 desired to skip apps loading for faster shell script execution
68 """
68 """
69 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
69 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
70 if ssh_cmd:
70 if ssh_cmd:
71 return False
71 return False
72
72
73 return True
73 return True
74
74
75
75
76 def make_pyramid_app(global_config, **settings):
76 def make_pyramid_app(global_config, **settings):
77 """
77 """
78 Constructs the WSGI application based on Pyramid.
78 Constructs the WSGI application based on Pyramid.
79
79
80 Specials:
80 Specials:
81
81
82 * The application can also be integrated like a plugin via the call to
82 * The application can also be integrated like a plugin via the call to
83 `includeme`. This is accompanied with the other utility functions which
83 `includeme`. This is accompanied with the other utility functions which
84 are called. Changing this should be done with great care to not break
84 are called. Changing this should be done with great care to not break
85 cases when these fragments are assembled from another place.
85 cases when these fragments are assembled from another place.
86
86
87 """
87 """
88 start_time = time.time()
88 start_time = time.time()
89 log.info('Pyramid app config starting')
89 log.info('Pyramid app config starting')
90
90
91 sanitize_settings_and_apply_defaults(global_config, settings)
91 sanitize_settings_and_apply_defaults(global_config, settings)
92
92
93 # init and bootstrap StatsdClient
93 # init and bootstrap StatsdClient
94 StatsdClient.setup(settings)
94 StatsdClient.setup(settings)
95
95
96 config = Configurator(settings=settings)
96 config = Configurator(settings=settings)
97 # Init our statsd at very start
97 # Init our statsd at very start
98 config.registry.statsd = StatsdClient.statsd
98 config.registry.statsd = StatsdClient.statsd
99
99
100 # Apply compatibility patches
100 # Apply compatibility patches
101 patches.inspect_getargspec()
101 patches.inspect_getargspec()
102 patches.repoze_sendmail_lf_fix()
102 patches.repoze_sendmail_lf_fix()
103
103
104 load_pyramid_environment(global_config, settings)
104 load_pyramid_environment(global_config, settings)
105
105
106 # Static file view comes first
106 # Static file view comes first
107 includeme_first(config)
107 includeme_first(config)
108
108
109 includeme(config)
109 includeme(config)
110
110
111 pyramid_app = config.make_wsgi_app()
111 pyramid_app = config.make_wsgi_app()
112 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
112 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
113 pyramid_app.config = config
113 pyramid_app.config = config
114
114
115 celery_settings = get_celery_config(settings)
115 celery_settings = get_celery_config(settings)
116 config.configure_celery(celery_settings)
116 config.configure_celery(celery_settings)
117
117
118 # final config set...
118 # final config set...
119 propagate_rhodecode_config(global_config, settings, config.registry.settings)
119 propagate_rhodecode_config(global_config, settings, config.registry.settings)
120
120
121 # creating the app uses a connection - return it after we are done
121 # creating the app uses a connection - return it after we are done
122 meta.Session.remove()
122 meta.Session.remove()
123
123
124 total_time = time.time() - start_time
124 total_time = time.time() - start_time
125 log.info('Pyramid app created and configured in %.2fs', total_time)
125 log.info('Pyramid app created and configured in %.2fs', total_time)
126 return pyramid_app
126 return pyramid_app
127
127
128
128
129 def get_celery_config(settings):
129 def get_celery_config(settings):
130 """
130 """
131 Converts basic ini configuration into celery 4.X options
131 Converts basic ini configuration into celery 4.X options
132 """
132 """
133
133
134 def key_converter(key_name):
134 def key_converter(key_name):
135 pref = 'celery.'
135 pref = 'celery.'
136 if key_name.startswith(pref):
136 if key_name.startswith(pref):
137 return key_name[len(pref):].replace('.', '_').lower()
137 return key_name[len(pref):].replace('.', '_').lower()
138
138
139 def type_converter(parsed_key, value):
139 def type_converter(parsed_key, value):
140 # cast to int
140 # cast to int
141 if value.isdigit():
141 if value.isdigit():
142 return int(value)
142 return int(value)
143
143
144 # cast to bool
144 # cast to bool
145 if value.lower() in ['true', 'false', 'True', 'False']:
145 if value.lower() in ['true', 'false', 'True', 'False']:
146 return value.lower() == 'true'
146 return value.lower() == 'true'
147 return value
147 return value
148
148
149 celery_config = {}
149 celery_config = {}
150 for k, v in settings.items():
150 for k, v in settings.items():
151 pref = 'celery.'
151 pref = 'celery.'
152 if k.startswith(pref):
152 if k.startswith(pref):
153 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
153 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
154
154
155 # TODO:rethink if we want to support celerybeat based file config, probably NOT
155 # TODO:rethink if we want to support celerybeat based file config, probably NOT
156 # beat_config = {}
156 # beat_config = {}
157 # for section in parser.sections():
157 # for section in parser.sections():
158 # if section.startswith('celerybeat:'):
158 # if section.startswith('celerybeat:'):
159 # name = section.split(':', 1)[1]
159 # name = section.split(':', 1)[1]
160 # beat_config[name] = get_beat_config(parser, section)
160 # beat_config[name] = get_beat_config(parser, section)
161
161
162 # final compose of settings
162 # final compose of settings
163 celery_settings = {}
163 celery_settings = {}
164
164
165 if celery_config:
165 if celery_config:
166 celery_settings.update(celery_config)
166 celery_settings.update(celery_config)
167 # if beat_config:
167 # if beat_config:
168 # celery_settings.update({'beat_schedule': beat_config})
168 # celery_settings.update({'beat_schedule': beat_config})
169
169
170 return celery_settings
170 return celery_settings
171
171
172
172
173 def not_found_view(request):
173 def not_found_view(request):
174 """
174 """
175 This creates the view which should be registered as not-found-view to
175 This creates the view which should be registered as not-found-view to
176 pyramid.
176 pyramid.
177 """
177 """
178
178
179 if not getattr(request, 'vcs_call', None):
179 if not getattr(request, 'vcs_call', None):
180 # handle like regular case with our error_handler
180 # handle like regular case with our error_handler
181 return error_handler(HTTPNotFound(), request)
181 return error_handler(HTTPNotFound(), request)
182
182
183 # handle not found view as a vcs call
183 # handle not found view as a vcs call
184 settings = request.registry.settings
184 settings = request.registry.settings
185 ae_client = getattr(request, 'ae_client', None)
185 ae_client = getattr(request, 'ae_client', None)
186 vcs_app = VCSMiddleware(
186 vcs_app = VCSMiddleware(
187 HTTPNotFound(), request.registry, settings,
187 HTTPNotFound(), request.registry, settings,
188 appenlight_client=ae_client)
188 appenlight_client=ae_client)
189
189
190 return wsgiapp(vcs_app)(None, request)
190 return wsgiapp(vcs_app)(None, request)
191
191
192
192
193 def error_handler(exception, request):
193 def error_handler(exception, request):
194 import rhodecode
194 import rhodecode
195 from rhodecode.lib import helpers
195 from rhodecode.lib import helpers
196
196
197 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
197 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
198
198
199 base_response = HTTPInternalServerError()
199 base_response = HTTPInternalServerError()
200 # prefer original exception for the response since it may have headers set
200 # prefer original exception for the response since it may have headers set
201 if isinstance(exception, HTTPException):
201 if isinstance(exception, HTTPException):
202 base_response = exception
202 base_response = exception
203 elif isinstance(exception, VCSCommunicationError):
203 elif isinstance(exception, VCSCommunicationError):
204 base_response = VCSServerUnavailable()
204 base_response = VCSServerUnavailable()
205
205
206 if is_http_error(base_response):
206 if is_http_error(base_response):
207 traceback_info = format_exc(request.exc_info)
207 traceback_info = format_exc(request.exc_info)
208 log.error(
208 log.error(
209 'error occurred handling this request for path: %s, \n%s',
209 'error occurred handling this request for path: %s, \n%s',
210 request.path, traceback_info)
210 request.path, traceback_info)
211
211
212 error_explanation = base_response.explanation or str(base_response)
212 error_explanation = base_response.explanation or str(base_response)
213 if base_response.status_code == 404:
213 if base_response.status_code == 404:
214 error_explanation += " Optionally you don't have permission to access this page."
214 error_explanation += " Optionally you don't have permission to access this page."
215 c = AttributeDict()
215 c = AttributeDict()
216 c.error_message = base_response.status
216 c.error_message = base_response.status
217 c.error_explanation = error_explanation
217 c.error_explanation = error_explanation
218 c.visual = AttributeDict()
218 c.visual = AttributeDict()
219
219
220 c.visual.rhodecode_support_url = (
220 c.visual.rhodecode_support_url = (
221 request.registry.settings.get('rhodecode_support_url') or
221 request.registry.settings.get('rhodecode_support_url') or
222 request.route_url('rhodecode_support')
222 request.route_url('rhodecode_support')
223 )
223 )
224 c.redirect_time = 0
224 c.redirect_time = 0
225 c.rhodecode_name = rhodecode_title
225 c.rhodecode_name = rhodecode_title
226 if not c.rhodecode_name:
226 if not c.rhodecode_name:
227 c.rhodecode_name = 'Rhodecode'
227 c.rhodecode_name = 'Rhodecode'
228
228
229 c.causes = []
229 c.causes = []
230 if is_http_error(base_response):
230 if is_http_error(base_response):
231 c.causes.append('Server is overloaded.')
231 c.causes.append('Server is overloaded.')
232 c.causes.append('Server database connection is lost.')
232 c.causes.append('Server database connection is lost.')
233 c.causes.append('Server expected unhandled error.')
233 c.causes.append('Server expected unhandled error.')
234
234
235 if hasattr(base_response, 'causes'):
235 if hasattr(base_response, 'causes'):
236 c.causes = base_response.causes
236 c.causes = base_response.causes
237
237
238 c.messages = helpers.flash.pop_messages(request=request)
238 c.messages = helpers.flash.pop_messages(request=request)
239 exc_info = sys.exc_info()
239 exc_info = sys.exc_info()
240 c.exception_id = id(exc_info)
240 c.exception_id = id(exc_info)
241 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
241 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
242 or base_response.status_code > 499
242 or base_response.status_code > 499
243 c.exception_id_url = request.route_url(
243 c.exception_id_url = request.route_url(
244 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
244 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
245
245
246 debug_mode = rhodecode.ConfigGet().get_bool('debug')
246 debug_mode = rhodecode.ConfigGet().get_bool('debug')
247 if c.show_exception_id:
247 if c.show_exception_id:
248 store_exception(c.exception_id, exc_info)
248 store_exception(c.exception_id, exc_info)
249 c.exception_debug = debug_mode
249 c.exception_debug = debug_mode
250 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
250 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
251
251
252 if debug_mode:
252 if debug_mode:
253 try:
253 try:
254 from rich.traceback import install
254 from rich.traceback import install
255 install(show_locals=True)
255 install(show_locals=True)
256 log.debug('Installing rich tracebacks...')
256 log.debug('Installing rich tracebacks...')
257 except ImportError:
257 except ImportError:
258 pass
258 pass
259
259
260 response = render_to_response(
260 response = render_to_response(
261 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
261 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
262 response=base_response)
262 response=base_response)
263
263
264 response.headers["X-RC-Exception-Id"] = str(c.exception_id)
264 response.headers["X-RC-Exception-Id"] = str(c.exception_id)
265
265
266 statsd = request.registry.statsd
266 statsd = request.registry.statsd
267 if statsd and base_response.status_code > 499:
267 if statsd and base_response.status_code > 499:
268 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
268 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
269 statsd.incr('rhodecode_exception_total',
269 statsd.incr('rhodecode_exception_total',
270 tags=["exc_source:web",
270 tags=["exc_source:web",
271 f"http_code:{base_response.status_code}",
271 f"http_code:{base_response.status_code}",
272 f"type:{exc_type}"])
272 f"type:{exc_type}"])
273
273
274 return response
274 return response
275
275
276
276
277 def includeme_first(config):
277 def includeme_first(config):
278 # redirect automatic browser favicon.ico requests to correct place
278 # redirect automatic browser favicon.ico requests to correct place
279 def favicon_redirect(context, request):
279 def favicon_redirect(context, request):
280 return HTTPFound(
280 return HTTPFound(
281 request.static_path('rhodecode:public/images/favicon.ico'))
281 request.static_path('rhodecode:public/images/favicon.ico'))
282
282
283 config.add_view(favicon_redirect, route_name='favicon')
283 config.add_view(favicon_redirect, route_name='favicon')
284 config.add_route('favicon', '/favicon.ico')
284 config.add_route('favicon', '/favicon.ico')
285
285
286 def robots_redirect(context, request):
286 def robots_redirect(context, request):
287 return HTTPFound(
287 return HTTPFound(
288 request.static_path('rhodecode:public/robots.txt'))
288 request.static_path('rhodecode:public/robots.txt'))
289
289
290 config.add_view(robots_redirect, route_name='robots')
290 config.add_view(robots_redirect, route_name='robots')
291 config.add_route('robots', '/robots.txt')
291 config.add_route('robots', '/robots.txt')
292
292
293 config.add_static_view(
293 config.add_static_view(
294 '_static/deform', 'deform:static')
294 '_static/deform', 'deform:static')
295 config.add_static_view(
295 config.add_static_view(
296 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
296 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
297
297
298
298
299 ce_auth_resources = [
299 ce_auth_resources = [
300 'rhodecode.authentication.plugins.auth_crowd',
300 'rhodecode.authentication.plugins.auth_crowd',
301 'rhodecode.authentication.plugins.auth_headers',
301 'rhodecode.authentication.plugins.auth_headers',
302 'rhodecode.authentication.plugins.auth_jasig_cas',
302 'rhodecode.authentication.plugins.auth_jasig_cas',
303 'rhodecode.authentication.plugins.auth_ldap',
303 'rhodecode.authentication.plugins.auth_ldap',
304 'rhodecode.authentication.plugins.auth_pam',
304 'rhodecode.authentication.plugins.auth_pam',
305 'rhodecode.authentication.plugins.auth_rhodecode',
305 'rhodecode.authentication.plugins.auth_rhodecode',
306 'rhodecode.authentication.plugins.auth_token',
306 'rhodecode.authentication.plugins.auth_token',
307 ]
307 ]
308
308
309
309
310 def includeme(config, auth_resources=None):
310 def includeme(config, auth_resources=None):
311 from rhodecode.lib.celerylib.loader import configure_celery
311 from rhodecode.lib.celerylib.loader import configure_celery
312 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
312 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
313 settings = config.registry.settings
313 settings = config.registry.settings
314 config.set_request_factory(Request)
314 config.set_request_factory(Request)
315
315
316 # plugin information
316 # plugin information
317 config.registry.rhodecode_plugins = collections.OrderedDict()
317 config.registry.rhodecode_plugins = collections.OrderedDict()
318
318
319 config.add_directive(
319 config.add_directive(
320 'register_rhodecode_plugin', register_rhodecode_plugin)
320 'register_rhodecode_plugin', register_rhodecode_plugin)
321
321
322 config.add_directive('configure_celery', configure_celery)
322 config.add_directive('configure_celery', configure_celery)
323
323
324 if settings.get('appenlight', False):
324 if settings.get('appenlight', False):
325 config.include('appenlight_client.ext.pyramid_tween')
325 config.include('appenlight_client.ext.pyramid_tween')
326
326
327 load_all = should_load_all()
327 load_all = should_load_all()
328
328
329 # Includes which are required. The application would fail without them.
329 # Includes which are required. The application would fail without them.
330 config.include('pyramid_mako')
330 config.include('pyramid_mako')
331 config.include('rhodecode.lib.rc_beaker')
331 config.include('rhodecode.lib.rc_beaker')
332 config.include('rhodecode.lib.rc_cache')
332 config.include('rhodecode.lib.rc_cache')
333 config.include('rhodecode.lib.archive_cache')
333 config.include('rhodecode.lib.archive_cache')
334
334
335 config.include('rhodecode.apps._base.navigation')
335 config.include('rhodecode.apps._base.navigation')
336 config.include('rhodecode.apps._base.subscribers')
336 config.include('rhodecode.apps._base.subscribers')
337 config.include('rhodecode.tweens')
337 config.include('rhodecode.tweens')
338 config.include('rhodecode.authentication')
338 config.include('rhodecode.authentication')
339
339
340 if load_all:
340 if load_all:
341
341
342 # load CE authentication plugins
342 # load CE authentication plugins
343
343
344 if auth_resources:
344 if auth_resources:
345 ce_auth_resources.extend(auth_resources)
345 ce_auth_resources.extend(auth_resources)
346
346
347 for resource in ce_auth_resources:
347 for resource in ce_auth_resources:
348 config.include(resource)
348 config.include(resource)
349
349
350 # Auto discover authentication plugins and include their configuration.
350 # Auto discover authentication plugins and include their configuration.
351 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
351 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
352 from rhodecode.authentication import discover_legacy_plugins
352 from rhodecode.authentication import discover_legacy_plugins
353 discover_legacy_plugins(config)
353 discover_legacy_plugins(config)
354
354
355 # apps
355 # apps
356 if load_all:
356 if load_all:
357 log.debug('Starting config.include() calls')
357 log.debug('Starting config.include() calls')
358 config.include('rhodecode.api.includeme')
358 config.include('rhodecode.api.includeme')
359 config.include('rhodecode.apps._base.includeme')
359 config.include('rhodecode.apps._base.includeme')
360 config.include('rhodecode.apps._base.navigation.includeme')
360 config.include('rhodecode.apps._base.navigation.includeme')
361 config.include('rhodecode.apps._base.subscribers.includeme')
361 config.include('rhodecode.apps._base.subscribers.includeme')
362 config.include('rhodecode.apps.hovercards.includeme')
362 config.include('rhodecode.apps.hovercards.includeme')
363 config.include('rhodecode.apps.ops.includeme')
363 config.include('rhodecode.apps.ops.includeme')
364 config.include('rhodecode.apps.channelstream.includeme')
364 config.include('rhodecode.apps.channelstream.includeme')
365 config.include('rhodecode.apps.file_store.includeme')
365 config.include('rhodecode.apps.file_store.includeme')
366 config.include('rhodecode.apps.admin.includeme')
366 config.include('rhodecode.apps.admin.includeme')
367 config.include('rhodecode.apps.login.includeme')
367 config.include('rhodecode.apps.login.includeme')
368 config.include('rhodecode.apps.home.includeme')
368 config.include('rhodecode.apps.home.includeme')
369 config.include('rhodecode.apps.journal.includeme')
369 config.include('rhodecode.apps.journal.includeme')
370
370
371 config.include('rhodecode.apps.repository.includeme')
371 config.include('rhodecode.apps.repository.includeme')
372 config.include('rhodecode.apps.repo_group.includeme')
372 config.include('rhodecode.apps.repo_group.includeme')
373 config.include('rhodecode.apps.user_group.includeme')
373 config.include('rhodecode.apps.user_group.includeme')
374 config.include('rhodecode.apps.search.includeme')
374 config.include('rhodecode.apps.search.includeme')
375 config.include('rhodecode.apps.user_profile.includeme')
375 config.include('rhodecode.apps.user_profile.includeme')
376 config.include('rhodecode.apps.user_group_profile.includeme')
376 config.include('rhodecode.apps.user_group_profile.includeme')
377 config.include('rhodecode.apps.my_account.includeme')
377 config.include('rhodecode.apps.my_account.includeme')
378 config.include('rhodecode.apps.gist.includeme')
378 config.include('rhodecode.apps.gist.includeme')
379
379
380 config.include('rhodecode.apps.svn_support.includeme')
380 config.include('rhodecode.apps.svn_support.includeme')
381 config.include('rhodecode.apps.ssh_support.includeme')
381 config.include('rhodecode.apps.ssh_support.includeme')
382 config.include('rhodecode.apps.debug_style')
382 config.include('rhodecode.apps.debug_style')
383
383
384 if load_all:
384 if load_all:
385 config.include('rhodecode.integrations.includeme')
385 config.include('rhodecode.integrations.includeme')
386 config.include('rhodecode.integrations.routes.includeme')
386 config.include('rhodecode.integrations.routes.includeme')
387
387
388 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
388 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
389 settings['default_locale_name'] = settings.get('lang', 'en')
389 settings['default_locale_name'] = settings.get('lang', 'en')
390 config.add_translation_dirs('rhodecode:i18n/')
390 config.add_translation_dirs('rhodecode:i18n/')
391
391
392 # Add subscribers.
392 # Add subscribers.
393 if load_all:
393 if load_all:
394 log.debug('Adding subscribers...')
394 log.debug('Adding subscribers...')
395 config.add_subscriber(auto_merge_pr_if_needed, rhodecode.events.PullRequestReviewEvent)
395 config.add_subscriber(scan_repositories_if_enabled,
396 config.add_subscriber(scan_repositories_if_enabled,
396 pyramid.events.ApplicationCreated)
397 pyramid.events.ApplicationCreated)
397 config.add_subscriber(write_metadata_if_needed,
398 config.add_subscriber(write_metadata_if_needed,
398 pyramid.events.ApplicationCreated)
399 pyramid.events.ApplicationCreated)
399 config.add_subscriber(write_usage_data,
400 config.add_subscriber(write_usage_data,
400 pyramid.events.ApplicationCreated)
401 pyramid.events.ApplicationCreated)
401 config.add_subscriber(write_js_routes_if_enabled,
402 config.add_subscriber(write_js_routes_if_enabled,
402 pyramid.events.ApplicationCreated)
403 pyramid.events.ApplicationCreated)
403 config.add_subscriber(import_license_if_present,
404 config.add_subscriber(import_license_if_present,
404 pyramid.events.ApplicationCreated)
405 pyramid.events.ApplicationCreated)
405
406
406 # Set the default renderer for HTML templates to mako.
407 # Set the default renderer for HTML templates to mako.
407 config.add_mako_renderer('.html')
408 config.add_mako_renderer('.html')
408
409
409 config.add_renderer(
410 config.add_renderer(
410 name='json_ext',
411 name='json_ext',
411 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
412 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
412
413
413 config.add_renderer(
414 config.add_renderer(
414 name='string_html',
415 name='string_html',
415 factory='rhodecode.lib.string_renderer.html')
416 factory='rhodecode.lib.string_renderer.html')
416
417
417 # include RhodeCode plugins
418 # include RhodeCode plugins
418 includes = aslist(settings.get('rhodecode.includes', []))
419 includes = aslist(settings.get('rhodecode.includes', []))
419 log.debug('processing rhodecode.includes data...')
420 log.debug('processing rhodecode.includes data...')
420 for inc in includes:
421 for inc in includes:
421 config.include(inc)
422 config.include(inc)
422
423
423 # custom not found view, if our pyramid app doesn't know how to handle
424 # custom not found view, if our pyramid app doesn't know how to handle
424 # the request pass it to potential VCS handling ap
425 # the request pass it to potential VCS handling ap
425 config.add_notfound_view(not_found_view)
426 config.add_notfound_view(not_found_view)
426 if not settings.get('debugtoolbar.enabled', False):
427 if not settings.get('debugtoolbar.enabled', False):
427 # disabled debugtoolbar handle all exceptions via the error_handlers
428 # disabled debugtoolbar handle all exceptions via the error_handlers
428 config.add_view(error_handler, context=Exception)
429 config.add_view(error_handler, context=Exception)
429
430
430 # all errors including 403/404/50X
431 # all errors including 403/404/50X
431 config.add_view(error_handler, context=HTTPError)
432 config.add_view(error_handler, context=HTTPError)
432
433
433
434
434 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
435 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
435 """
436 """
436 Apply outer WSGI middlewares around the application.
437 Apply outer WSGI middlewares around the application.
437 """
438 """
438 registry = config.registry
439 registry = config.registry
439 settings = registry.settings
440 settings = registry.settings
440
441
441 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
442 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
442 pyramid_app = HttpsFixup(pyramid_app, settings)
443 pyramid_app = HttpsFixup(pyramid_app, settings)
443
444
444 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
445 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
445 pyramid_app, settings)
446 pyramid_app, settings)
446 registry.ae_client = _ae_client
447 registry.ae_client = _ae_client
447
448
448 if settings['gzip_responses']:
449 if settings['gzip_responses']:
449 pyramid_app = make_gzip_middleware(
450 pyramid_app = make_gzip_middleware(
450 pyramid_app, settings, compress_level=1)
451 pyramid_app, settings, compress_level=1)
451
452
452 # this should be the outer most middleware in the wsgi stack since
453 # this should be the outer most middleware in the wsgi stack since
453 # middleware like Routes make database calls
454 # middleware like Routes make database calls
454 def pyramid_app_with_cleanup(environ, start_response):
455 def pyramid_app_with_cleanup(environ, start_response):
455 start = time.time()
456 start = time.time()
456 try:
457 try:
457 return pyramid_app(environ, start_response)
458 return pyramid_app(environ, start_response)
458 finally:
459 finally:
459 # Dispose current database session and rollback uncommitted
460 # Dispose current database session and rollback uncommitted
460 # transactions.
461 # transactions.
461 meta.Session.remove()
462 meta.Session.remove()
462
463
463 # In a single threaded mode server, on non sqlite db we should have
464 # In a single threaded mode server, on non sqlite db we should have
464 # '0 Current Checked out connections' at the end of a request,
465 # '0 Current Checked out connections' at the end of a request,
465 # if not, then something, somewhere is leaving a connection open
466 # if not, then something, somewhere is leaving a connection open
466 pool = meta.get_engine().pool
467 pool = meta.get_engine().pool
467 log.debug('sa pool status: %s', pool.status())
468 log.debug('sa pool status: %s', pool.status())
468 total = time.time() - start
469 total = time.time() - start
469 log.debug('Request processing finalized: %.4fs', total)
470 log.debug('Request processing finalized: %.4fs', total)
470
471
471 return pyramid_app_with_cleanup
472 return pyramid_app_with_cleanup
@@ -1,662 +1,663
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 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 this is forms validation classes
20 this is forms validation classes
21 http://formencode.org/module-formencode.validators.html
21 http://formencode.org/module-formencode.validators.html
22 for list off all availible validators
22 for list off all availible validators
23
23
24 we can create our own validators
24 we can create our own validators
25
25
26 The table below outlines the options which can be used in a schema in addition to the validators themselves
26 The table below outlines the options which can be used in a schema in addition to the validators themselves
27 pre_validators [] These validators will be applied before the schema
27 pre_validators [] These validators will be applied before the schema
28 chained_validators [] These validators will be applied after the schema
28 chained_validators [] These validators will be applied after the schema
29 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
29 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
30 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
30 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
31 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
31 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
32 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
32 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
33
33
34
34
35 <name> = formencode.validators.<name of validator>
35 <name> = formencode.validators.<name of validator>
36 <name> must equal form name
36 <name> must equal form name
37 list=[1,2,3,4,5]
37 list=[1,2,3,4,5]
38 for SELECT use formencode.All(OneOf(list), Int())
38 for SELECT use formencode.All(OneOf(list), Int())
39
39
40 """
40 """
41
41
42 import deform
42 import deform
43 import logging
43 import logging
44 import formencode
44 import formencode
45
45
46 from pkg_resources import resource_filename
46 from pkg_resources import resource_filename
47 from formencode import All, Pipe
47 from formencode import All, Pipe
48
48
49 from pyramid.threadlocal import get_current_request
49 from pyramid.threadlocal import get_current_request
50
50
51 from rhodecode import BACKENDS
51 from rhodecode import BACKENDS
52 from rhodecode.lib import helpers
52 from rhodecode.lib import helpers
53 from rhodecode.model import validators as v
53 from rhodecode.model import validators as v
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 deform_templates = resource_filename('deform', 'templates')
58 deform_templates = resource_filename('deform', 'templates')
59 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
59 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
60 search_path = (rhodecode_templates, deform_templates)
60 search_path = (rhodecode_templates, deform_templates)
61
61
62
62
63 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
63 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
64 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
64 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
65 def __call__(self, template_name, **kw):
65 def __call__(self, template_name, **kw):
66 kw['h'] = helpers
66 kw['h'] = helpers
67 kw['request'] = get_current_request()
67 kw['request'] = get_current_request()
68 return self.load(template_name)(**kw)
68 return self.load(template_name)(**kw)
69
69
70
70
71 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
71 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
72 deform.Form.set_default_renderer(form_renderer)
72 deform.Form.set_default_renderer(form_renderer)
73
73
74
74
75 def LoginForm(localizer):
75 def LoginForm(localizer):
76 _ = localizer
76 _ = localizer
77
77
78 class _LoginForm(formencode.Schema):
78 class _LoginForm(formencode.Schema):
79 allow_extra_fields = True
79 allow_extra_fields = True
80 filter_extra_fields = True
80 filter_extra_fields = True
81 username = v.UnicodeString(
81 username = v.UnicodeString(
82 strip=True,
82 strip=True,
83 min=1,
83 min=1,
84 not_empty=True,
84 not_empty=True,
85 messages={
85 messages={
86 'empty': _('Please enter a login'),
86 'empty': _('Please enter a login'),
87 'tooShort': _('Enter a value %(min)i characters long or more')
87 'tooShort': _('Enter a value %(min)i characters long or more')
88 }
88 }
89 )
89 )
90
90
91 password = v.UnicodeString(
91 password = v.UnicodeString(
92 strip=False,
92 strip=False,
93 min=3,
93 min=3,
94 max=72,
94 max=72,
95 not_empty=True,
95 not_empty=True,
96 messages={
96 messages={
97 'empty': _('Please enter a password'),
97 'empty': _('Please enter a password'),
98 'tooShort': _('Enter %(min)i characters or more')}
98 'tooShort': _('Enter %(min)i characters or more')}
99 )
99 )
100
100
101 remember = v.StringBoolean(if_missing=False)
101 remember = v.StringBoolean(if_missing=False)
102
102
103 chained_validators = [v.ValidAuth(localizer)]
103 chained_validators = [v.ValidAuth(localizer)]
104 return _LoginForm
104 return _LoginForm
105
105
106
106
107 def TOTPForm(localizer, user, allow_recovery_code_use=False):
107 def TOTPForm(localizer, user, allow_recovery_code_use=False):
108 _ = localizer
108 _ = localizer
109
109
110 class _TOTPForm(formencode.Schema):
110 class _TOTPForm(formencode.Schema):
111 allow_extra_fields = True
111 allow_extra_fields = True
112 filter_extra_fields = False
112 filter_extra_fields = False
113 totp = v.Regex(r'^(?:\d{6}|[A-Z0-9]{32})$')
113 totp = v.Regex(r'^(?:\d{6}|[A-Z0-9]{32})$')
114 secret_totp = v.String()
114 secret_totp = v.String()
115
115
116 def to_python(self, value, state=None):
116 def to_python(self, value, state=None):
117 validation_checks = [user.is_totp_valid]
117 validation_checks = [user.is_totp_valid]
118 if allow_recovery_code_use:
118 if allow_recovery_code_use:
119 validation_checks.append(user.is_2fa_recovery_code_valid)
119 validation_checks.append(user.is_2fa_recovery_code_valid)
120 form_data = super().to_python(value, state)
120 form_data = super().to_python(value, state)
121 received_code = form_data['totp']
121 received_code = form_data['totp']
122 secret = form_data.get('secret_totp')
122 secret = form_data.get('secret_totp')
123
123
124 if not any(map(lambda func: func(received_code, secret), validation_checks)):
124 if not any(map(lambda func: func(received_code, secret), validation_checks)):
125 error_msg = _('Code is invalid. Try again!')
125 error_msg = _('Code is invalid. Try again!')
126 raise formencode.Invalid(error_msg, v, state, error_dict={'totp': error_msg})
126 raise formencode.Invalid(error_msg, v, state, error_dict={'totp': error_msg})
127 return form_data
127 return form_data
128
128
129 return _TOTPForm
129 return _TOTPForm
130
130
131
131
132 def WhitelistedVcsClientsForm(localizer):
132 def WhitelistedVcsClientsForm(localizer):
133 _ = localizer
133 _ = localizer
134
134
135 class _WhitelistedVcsClientsForm(formencode.Schema):
135 class _WhitelistedVcsClientsForm(formencode.Schema):
136 regexp = r'^(?:\s*[<>=~^!]*\s*\d{1,2}\.\d{1,2}(?:\.\d{1,2})?\s*|\*)\s*(?:,\s*[<>=~^!]*\s*\d{1,2}\.\d{1,2}(?:\.\d{1,2})?\s*|\s*\*\s*)*$'
136 regexp = r'^(?:\s*[<>=~^!]*\s*\d{1,2}\.\d{1,2}(?:\.\d{1,2})?\s*|\*)\s*(?:,\s*[<>=~^!]*\s*\d{1,2}\.\d{1,2}(?:\.\d{1,2})?\s*|\s*\*\s*)*$'
137 allow_extra_fields = True
137 allow_extra_fields = True
138 filter_extra_fields = True
138 filter_extra_fields = True
139 git = v.Regex(regexp)
139 git = v.Regex(regexp)
140 hg = v.Regex(regexp)
140 hg = v.Regex(regexp)
141 svn = v.Regex(regexp)
141 svn = v.Regex(regexp)
142
142
143 return _WhitelistedVcsClientsForm
143 return _WhitelistedVcsClientsForm
144
144
145
145
146 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
146 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
147 old_data = old_data or {}
147 old_data = old_data or {}
148 available_languages = available_languages or []
148 available_languages = available_languages or []
149 _ = localizer
149 _ = localizer
150
150
151 class _UserForm(formencode.Schema):
151 class _UserForm(formencode.Schema):
152 allow_extra_fields = True
152 allow_extra_fields = True
153 filter_extra_fields = True
153 filter_extra_fields = True
154 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
154 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
155 v.ValidUsername(localizer, edit, old_data))
155 v.ValidUsername(localizer, edit, old_data))
156 if edit:
156 if edit:
157 new_password = All(
157 new_password = All(
158 v.ValidPassword(localizer),
158 v.ValidPassword(localizer),
159 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
159 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
160 )
160 )
161 password_confirmation = All(
161 password_confirmation = All(
162 v.ValidPassword(localizer),
162 v.ValidPassword(localizer),
163 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
163 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
164 )
164 )
165 admin = v.StringBoolean(if_missing=False)
165 admin = v.StringBoolean(if_missing=False)
166 else:
166 else:
167 password = All(
167 password = All(
168 v.ValidPassword(localizer),
168 v.ValidPassword(localizer),
169 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
169 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
170 )
170 )
171 password_confirmation = All(
171 password_confirmation = All(
172 v.ValidPassword(localizer),
172 v.ValidPassword(localizer),
173 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
173 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
174 )
174 )
175
175
176 password_change = v.StringBoolean(if_missing=False)
176 password_change = v.StringBoolean(if_missing=False)
177 create_repo_group = v.StringBoolean(if_missing=False)
177 create_repo_group = v.StringBoolean(if_missing=False)
178
178
179 active = v.StringBoolean(if_missing=False)
179 active = v.StringBoolean(if_missing=False)
180 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
180 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
181 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
181 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
182 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
182 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
183 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
183 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
184 if_missing='')
184 if_missing='')
185 extern_name = v.UnicodeString(strip=True)
185 extern_name = v.UnicodeString(strip=True)
186 extern_type = v.UnicodeString(strip=True)
186 extern_type = v.UnicodeString(strip=True)
187 language = v.OneOf(available_languages, hideList=False,
187 language = v.OneOf(available_languages, hideList=False,
188 testValueList=True, if_missing=None)
188 testValueList=True, if_missing=None)
189 chained_validators = [v.ValidPasswordsMatch(localizer)]
189 chained_validators = [v.ValidPasswordsMatch(localizer)]
190 return _UserForm
190 return _UserForm
191
191
192
192
193 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
193 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
194 old_data = old_data or {}
194 old_data = old_data or {}
195 _ = localizer
195 _ = localizer
196
196
197 class _UserGroupForm(formencode.Schema):
197 class _UserGroupForm(formencode.Schema):
198 allow_extra_fields = True
198 allow_extra_fields = True
199 filter_extra_fields = True
199 filter_extra_fields = True
200
200
201 users_group_name = All(
201 users_group_name = All(
202 v.UnicodeString(strip=True, min=1, not_empty=True),
202 v.UnicodeString(strip=True, min=1, not_empty=True),
203 v.ValidUserGroup(localizer, edit, old_data)
203 v.ValidUserGroup(localizer, edit, old_data)
204 )
204 )
205 user_group_description = v.UnicodeString(strip=True, min=1,
205 user_group_description = v.UnicodeString(strip=True, min=1,
206 not_empty=False)
206 not_empty=False)
207
207
208 users_group_active = v.StringBoolean(if_missing=False)
208 users_group_active = v.StringBoolean(if_missing=False)
209
209
210 if edit:
210 if edit:
211 # this is user group owner
211 # this is user group owner
212 user = All(
212 user = All(
213 v.UnicodeString(not_empty=True),
213 v.UnicodeString(not_empty=True),
214 v.ValidRepoUser(localizer, allow_disabled))
214 v.ValidRepoUser(localizer, allow_disabled))
215 return _UserGroupForm
215 return _UserGroupForm
216
216
217
217
218 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
218 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
219 can_create_in_root=False, allow_disabled=False):
219 can_create_in_root=False, allow_disabled=False):
220 _ = localizer
220 _ = localizer
221 old_data = old_data or {}
221 old_data = old_data or {}
222 available_groups = available_groups or []
222 available_groups = available_groups or []
223
223
224 class _RepoGroupForm(formencode.Schema):
224 class _RepoGroupForm(formencode.Schema):
225 allow_extra_fields = True
225 allow_extra_fields = True
226 filter_extra_fields = False
226 filter_extra_fields = False
227
227
228 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
228 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
229 v.SlugifyName(localizer),)
229 v.SlugifyName(localizer),)
230 group_description = v.UnicodeString(strip=True, min=1,
230 group_description = v.UnicodeString(strip=True, min=1,
231 not_empty=False)
231 not_empty=False)
232 group_copy_permissions = v.StringBoolean(if_missing=False)
232 group_copy_permissions = v.StringBoolean(if_missing=False)
233
233
234 group_parent_id = v.OneOf(available_groups, hideList=False,
234 group_parent_id = v.OneOf(available_groups, hideList=False,
235 testValueList=True, not_empty=True)
235 testValueList=True, not_empty=True)
236 enable_locking = v.StringBoolean(if_missing=False)
236 enable_locking = v.StringBoolean(if_missing=False)
237 chained_validators = [
237 chained_validators = [
238 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
238 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
239
239
240 if edit:
240 if edit:
241 # this is repo group owner
241 # this is repo group owner
242 user = All(
242 user = All(
243 v.UnicodeString(not_empty=True),
243 v.UnicodeString(not_empty=True),
244 v.ValidRepoUser(localizer, allow_disabled))
244 v.ValidRepoUser(localizer, allow_disabled))
245 return _RepoGroupForm
245 return _RepoGroupForm
246
246
247
247
248 def RegisterForm(localizer, edit=False, old_data=None):
248 def RegisterForm(localizer, edit=False, old_data=None):
249 _ = localizer
249 _ = localizer
250 old_data = old_data or {}
250 old_data = old_data or {}
251
251
252 class _RegisterForm(formencode.Schema):
252 class _RegisterForm(formencode.Schema):
253 allow_extra_fields = True
253 allow_extra_fields = True
254 filter_extra_fields = True
254 filter_extra_fields = True
255 username = All(
255 username = All(
256 v.ValidUsername(localizer, edit, old_data),
256 v.ValidUsername(localizer, edit, old_data),
257 v.UnicodeString(strip=True, min=1, not_empty=True)
257 v.UnicodeString(strip=True, min=1, not_empty=True)
258 )
258 )
259 password = All(
259 password = All(
260 v.ValidPassword(localizer),
260 v.ValidPassword(localizer),
261 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
261 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
262 )
262 )
263 password_confirmation = All(
263 password_confirmation = All(
264 v.ValidPassword(localizer),
264 v.ValidPassword(localizer),
265 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
265 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
266 )
266 )
267 active = v.StringBoolean(if_missing=False)
267 active = v.StringBoolean(if_missing=False)
268 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
268 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
269 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
269 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
270 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
270 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
271
271
272 chained_validators = [v.ValidPasswordsMatch(localizer)]
272 chained_validators = [v.ValidPasswordsMatch(localizer)]
273 return _RegisterForm
273 return _RegisterForm
274
274
275
275
276 def PasswordResetForm(localizer):
276 def PasswordResetForm(localizer):
277 _ = localizer
277 _ = localizer
278
278
279 class _PasswordResetForm(formencode.Schema):
279 class _PasswordResetForm(formencode.Schema):
280 allow_extra_fields = True
280 allow_extra_fields = True
281 filter_extra_fields = True
281 filter_extra_fields = True
282 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
282 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
283 return _PasswordResetForm
283 return _PasswordResetForm
284
284
285
285
286 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
286 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
287 _ = localizer
287 _ = localizer
288 old_data = old_data or {}
288 old_data = old_data or {}
289 repo_groups = repo_groups or []
289 repo_groups = repo_groups or []
290 supported_backends = BACKENDS.keys()
290 supported_backends = BACKENDS.keys()
291
291
292 class _RepoForm(formencode.Schema):
292 class _RepoForm(formencode.Schema):
293 allow_extra_fields = True
293 allow_extra_fields = True
294 filter_extra_fields = False
294 filter_extra_fields = False
295 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
295 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
296 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
296 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
297 repo_group = All(v.CanWriteGroup(localizer, old_data),
297 repo_group = All(v.CanWriteGroup(localizer, old_data),
298 v.OneOf(repo_groups, hideList=True))
298 v.OneOf(repo_groups, hideList=True))
299 repo_type = v.OneOf(supported_backends, required=False,
299 repo_type = v.OneOf(supported_backends, required=False,
300 if_missing=old_data.get('repo_type'))
300 if_missing=old_data.get('repo_type'))
301 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
301 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
302 repo_private = v.StringBoolean(if_missing=False)
302 repo_private = v.StringBoolean(if_missing=False)
303 repo_copy_permissions = v.StringBoolean(if_missing=False)
303 repo_copy_permissions = v.StringBoolean(if_missing=False)
304 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
304 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
305
305
306 repo_enable_statistics = v.StringBoolean(if_missing=False)
306 repo_enable_statistics = v.StringBoolean(if_missing=False)
307 repo_enable_downloads = v.StringBoolean(if_missing=False)
307 repo_enable_downloads = v.StringBoolean(if_missing=False)
308 repo_enable_locking = v.StringBoolean(if_missing=False)
308 repo_enable_locking = v.StringBoolean(if_missing=False)
309
309
310 if edit:
310 if edit:
311 # this is repo owner
311 # this is repo owner
312 user = All(
312 user = All(
313 v.UnicodeString(not_empty=True),
313 v.UnicodeString(not_empty=True),
314 v.ValidRepoUser(localizer, allow_disabled))
314 v.ValidRepoUser(localizer, allow_disabled))
315 clone_uri_change = v.UnicodeString(
315 clone_uri_change = v.UnicodeString(
316 not_empty=False, if_missing=v.Missing)
316 not_empty=False, if_missing=v.Missing)
317
317
318 chained_validators = [v.ValidCloneUri(localizer),
318 chained_validators = [v.ValidCloneUri(localizer),
319 v.ValidRepoName(localizer, edit, old_data)]
319 v.ValidRepoName(localizer, edit, old_data)]
320 return _RepoForm
320 return _RepoForm
321
321
322
322
323 def RepoPermsForm(localizer):
323 def RepoPermsForm(localizer):
324 _ = localizer
324 _ = localizer
325
325
326 class _RepoPermsForm(formencode.Schema):
326 class _RepoPermsForm(formencode.Schema):
327 allow_extra_fields = True
327 allow_extra_fields = True
328 filter_extra_fields = False
328 filter_extra_fields = False
329 chained_validators = [v.ValidPerms(localizer, type_='repo')]
329 chained_validators = [v.ValidPerms(localizer, type_='repo')]
330 return _RepoPermsForm
330 return _RepoPermsForm
331
331
332
332
333 def RepoGroupPermsForm(localizer, valid_recursive_choices):
333 def RepoGroupPermsForm(localizer, valid_recursive_choices):
334 _ = localizer
334 _ = localizer
335
335
336 class _RepoGroupPermsForm(formencode.Schema):
336 class _RepoGroupPermsForm(formencode.Schema):
337 allow_extra_fields = True
337 allow_extra_fields = True
338 filter_extra_fields = False
338 filter_extra_fields = False
339 recursive = v.OneOf(valid_recursive_choices)
339 recursive = v.OneOf(valid_recursive_choices)
340 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
340 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
341 return _RepoGroupPermsForm
341 return _RepoGroupPermsForm
342
342
343
343
344 def UserGroupPermsForm(localizer):
344 def UserGroupPermsForm(localizer):
345 _ = localizer
345 _ = localizer
346
346
347 class _UserPermsForm(formencode.Schema):
347 class _UserPermsForm(formencode.Schema):
348 allow_extra_fields = True
348 allow_extra_fields = True
349 filter_extra_fields = False
349 filter_extra_fields = False
350 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
350 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
351 return _UserPermsForm
351 return _UserPermsForm
352
352
353
353
354 def RepoFieldForm(localizer):
354 def RepoFieldForm(localizer):
355 _ = localizer
355 _ = localizer
356
356
357 class _RepoFieldForm(formencode.Schema):
357 class _RepoFieldForm(formencode.Schema):
358 filter_extra_fields = True
358 filter_extra_fields = True
359 allow_extra_fields = True
359 allow_extra_fields = True
360
360
361 new_field_key = All(v.FieldKey(localizer),
361 new_field_key = All(v.FieldKey(localizer),
362 v.UnicodeString(strip=True, min=3, not_empty=True))
362 v.UnicodeString(strip=True, min=3, not_empty=True))
363 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
363 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
364 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
364 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
365 if_missing='str')
365 if_missing='str')
366 new_field_label = v.UnicodeString(not_empty=False)
366 new_field_label = v.UnicodeString(not_empty=False)
367 new_field_desc = v.UnicodeString(not_empty=False)
367 new_field_desc = v.UnicodeString(not_empty=False)
368 return _RepoFieldForm
368 return _RepoFieldForm
369
369
370
370
371 def RepoForkForm(localizer, edit=False, old_data=None,
371 def RepoForkForm(localizer, edit=False, old_data=None,
372 supported_backends=BACKENDS.keys(), repo_groups=None):
372 supported_backends=BACKENDS.keys(), repo_groups=None):
373 _ = localizer
373 _ = localizer
374 old_data = old_data or {}
374 old_data = old_data or {}
375 repo_groups = repo_groups or []
375 repo_groups = repo_groups or []
376
376
377 class _RepoForkForm(formencode.Schema):
377 class _RepoForkForm(formencode.Schema):
378 allow_extra_fields = True
378 allow_extra_fields = True
379 filter_extra_fields = False
379 filter_extra_fields = False
380 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
380 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
381 v.SlugifyName(localizer))
381 v.SlugifyName(localizer))
382 repo_group = All(v.CanWriteGroup(localizer, ),
382 repo_group = All(v.CanWriteGroup(localizer, ),
383 v.OneOf(repo_groups, hideList=True))
383 v.OneOf(repo_groups, hideList=True))
384 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
384 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
385 description = v.UnicodeString(strip=True, min=1, not_empty=True)
385 description = v.UnicodeString(strip=True, min=1, not_empty=True)
386 private = v.StringBoolean(if_missing=False)
386 private = v.StringBoolean(if_missing=False)
387 copy_permissions = v.StringBoolean(if_missing=False)
387 copy_permissions = v.StringBoolean(if_missing=False)
388 fork_parent_id = v.UnicodeString()
388 fork_parent_id = v.UnicodeString()
389 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
389 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
390 return _RepoForkForm
390 return _RepoForkForm
391
391
392
392
393 def ApplicationSettingsForm(localizer):
393 def ApplicationSettingsForm(localizer):
394 _ = localizer
394 _ = localizer
395
395
396 class _ApplicationSettingsForm(formencode.Schema):
396 class _ApplicationSettingsForm(formencode.Schema):
397 allow_extra_fields = True
397 allow_extra_fields = True
398 filter_extra_fields = False
398 filter_extra_fields = False
399 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
399 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
400 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
400 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
401 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
401 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
402 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
402 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
403 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
403 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
404 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
404 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
405 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
405 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
406 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
406 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
407 return _ApplicationSettingsForm
407 return _ApplicationSettingsForm
408
408
409
409
410 def ApplicationVisualisationForm(localizer):
410 def ApplicationVisualisationForm(localizer):
411 from rhodecode.model.db import Repository
411 from rhodecode.model.db import Repository
412 _ = localizer
412 _ = localizer
413
413
414 class _ApplicationVisualisationForm(formencode.Schema):
414 class _ApplicationVisualisationForm(formencode.Schema):
415 allow_extra_fields = True
415 allow_extra_fields = True
416 filter_extra_fields = False
416 filter_extra_fields = False
417 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
417 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
418 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
418 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
419 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
419 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
420
420
421 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
421 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
422 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
422 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
423 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
423 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
424 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
424 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
425 rhodecode_show_version = v.StringBoolean(if_missing=False)
425 rhodecode_show_version = v.StringBoolean(if_missing=False)
426 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
426 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
427 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
427 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
428 rhodecode_gravatar_url = v.UnicodeString(min=3)
428 rhodecode_gravatar_url = v.UnicodeString(min=3)
429 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
429 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
430 rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID)
430 rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID)
431 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
431 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
432 rhodecode_support_url = v.UnicodeString()
432 rhodecode_support_url = v.UnicodeString()
433 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
433 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
434 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
434 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
435 return _ApplicationVisualisationForm
435 return _ApplicationVisualisationForm
436
436
437
437
438 class _BaseVcsSettingsForm(formencode.Schema):
438 class _BaseVcsSettingsForm(formencode.Schema):
439
439
440 allow_extra_fields = True
440 allow_extra_fields = True
441 filter_extra_fields = False
441 filter_extra_fields = False
442 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
442 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
443 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
443 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
444 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
444 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
445
445
446 # PR/Code-review
446 # PR/Code-review
447 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
447 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
448 rhodecode_auto_merge_enabled = v.StringBoolean(if_missing=False)
448 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
449 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
449
450
450 # hg
451 # hg
451 extensions_largefiles = v.StringBoolean(if_missing=False)
452 extensions_largefiles = v.StringBoolean(if_missing=False)
452 extensions_evolve = v.StringBoolean(if_missing=False)
453 extensions_evolve = v.StringBoolean(if_missing=False)
453 phases_publish = v.StringBoolean(if_missing=False)
454 phases_publish = v.StringBoolean(if_missing=False)
454
455
455 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
456 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
456 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
457 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
457
458
458 # git
459 # git
459 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
460 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
460 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
461 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
461 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
462 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
462
463
463 # cache
464 # cache
464 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
465 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
465
466
466
467
467 def ApplicationUiSettingsForm(localizer):
468 def ApplicationUiSettingsForm(localizer):
468 _ = localizer
469 _ = localizer
469
470
470 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
471 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
471 extensions_hggit = v.StringBoolean(if_missing=False)
472 extensions_hggit = v.StringBoolean(if_missing=False)
472 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
473 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
473 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
474 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
474 return _ApplicationUiSettingsForm
475 return _ApplicationUiSettingsForm
475
476
476
477
477 def RepoVcsSettingsForm(localizer, repo_name):
478 def RepoVcsSettingsForm(localizer, repo_name):
478 _ = localizer
479 _ = localizer
479
480
480 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
481 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
481 inherit_global_settings = v.StringBoolean(if_missing=False)
482 inherit_global_settings = v.StringBoolean(if_missing=False)
482 new_svn_branch = v.ValidSvnPattern(localizer,
483 new_svn_branch = v.ValidSvnPattern(localizer,
483 section='vcs_svn_branch', repo_name=repo_name)
484 section='vcs_svn_branch', repo_name=repo_name)
484 new_svn_tag = v.ValidSvnPattern(localizer,
485 new_svn_tag = v.ValidSvnPattern(localizer,
485 section='vcs_svn_tag', repo_name=repo_name)
486 section='vcs_svn_tag', repo_name=repo_name)
486 return _RepoVcsSettingsForm
487 return _RepoVcsSettingsForm
487
488
488
489
489 def LabsSettingsForm(localizer):
490 def LabsSettingsForm(localizer):
490 _ = localizer
491 _ = localizer
491
492
492 class _LabSettingsForm(formencode.Schema):
493 class _LabSettingsForm(formencode.Schema):
493 allow_extra_fields = True
494 allow_extra_fields = True
494 filter_extra_fields = False
495 filter_extra_fields = False
495 return _LabSettingsForm
496 return _LabSettingsForm
496
497
497
498
498 def ApplicationPermissionsForm(
499 def ApplicationPermissionsForm(
499 localizer, register_choices, password_reset_choices,
500 localizer, register_choices, password_reset_choices,
500 extern_activate_choices):
501 extern_activate_choices):
501 _ = localizer
502 _ = localizer
502
503
503 class _DefaultPermissionsForm(formencode.Schema):
504 class _DefaultPermissionsForm(formencode.Schema):
504 allow_extra_fields = True
505 allow_extra_fields = True
505 filter_extra_fields = True
506 filter_extra_fields = True
506
507
507 anonymous = v.StringBoolean(if_missing=False)
508 anonymous = v.StringBoolean(if_missing=False)
508 default_register = v.OneOf(register_choices)
509 default_register = v.OneOf(register_choices)
509 default_register_message = v.UnicodeString()
510 default_register_message = v.UnicodeString()
510 default_password_reset = v.OneOf(password_reset_choices)
511 default_password_reset = v.OneOf(password_reset_choices)
511 default_extern_activate = v.OneOf(extern_activate_choices)
512 default_extern_activate = v.OneOf(extern_activate_choices)
512 return _DefaultPermissionsForm
513 return _DefaultPermissionsForm
513
514
514
515
515 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
516 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
516 user_group_perms_choices):
517 user_group_perms_choices):
517 _ = localizer
518 _ = localizer
518
519
519 class _ObjectPermissionsForm(formencode.Schema):
520 class _ObjectPermissionsForm(formencode.Schema):
520 allow_extra_fields = True
521 allow_extra_fields = True
521 filter_extra_fields = True
522 filter_extra_fields = True
522 overwrite_default_repo = v.StringBoolean(if_missing=False)
523 overwrite_default_repo = v.StringBoolean(if_missing=False)
523 overwrite_default_group = v.StringBoolean(if_missing=False)
524 overwrite_default_group = v.StringBoolean(if_missing=False)
524 overwrite_default_user_group = v.StringBoolean(if_missing=False)
525 overwrite_default_user_group = v.StringBoolean(if_missing=False)
525
526
526 default_repo_perm = v.OneOf(repo_perms_choices)
527 default_repo_perm = v.OneOf(repo_perms_choices)
527 default_group_perm = v.OneOf(group_perms_choices)
528 default_group_perm = v.OneOf(group_perms_choices)
528 default_user_group_perm = v.OneOf(user_group_perms_choices)
529 default_user_group_perm = v.OneOf(user_group_perms_choices)
529
530
530 return _ObjectPermissionsForm
531 return _ObjectPermissionsForm
531
532
532
533
533 def BranchPermissionsForm(localizer, branch_perms_choices):
534 def BranchPermissionsForm(localizer, branch_perms_choices):
534 _ = localizer
535 _ = localizer
535
536
536 class _BranchPermissionsForm(formencode.Schema):
537 class _BranchPermissionsForm(formencode.Schema):
537 allow_extra_fields = True
538 allow_extra_fields = True
538 filter_extra_fields = True
539 filter_extra_fields = True
539 overwrite_default_branch = v.StringBoolean(if_missing=False)
540 overwrite_default_branch = v.StringBoolean(if_missing=False)
540 default_branch_perm = v.OneOf(branch_perms_choices)
541 default_branch_perm = v.OneOf(branch_perms_choices)
541
542
542 return _BranchPermissionsForm
543 return _BranchPermissionsForm
543
544
544
545
545 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
546 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
546 repo_group_create_choices, user_group_create_choices,
547 repo_group_create_choices, user_group_create_choices,
547 fork_choices, inherit_default_permissions_choices):
548 fork_choices, inherit_default_permissions_choices):
548 _ = localizer
549 _ = localizer
549
550
550 class _DefaultPermissionsForm(formencode.Schema):
551 class _DefaultPermissionsForm(formencode.Schema):
551 allow_extra_fields = True
552 allow_extra_fields = True
552 filter_extra_fields = True
553 filter_extra_fields = True
553
554
554 anonymous = v.StringBoolean(if_missing=False)
555 anonymous = v.StringBoolean(if_missing=False)
555
556
556 default_repo_create = v.OneOf(create_choices)
557 default_repo_create = v.OneOf(create_choices)
557 default_repo_create_on_write = v.OneOf(create_on_write_choices)
558 default_repo_create_on_write = v.OneOf(create_on_write_choices)
558 default_user_group_create = v.OneOf(user_group_create_choices)
559 default_user_group_create = v.OneOf(user_group_create_choices)
559 default_repo_group_create = v.OneOf(repo_group_create_choices)
560 default_repo_group_create = v.OneOf(repo_group_create_choices)
560 default_fork_create = v.OneOf(fork_choices)
561 default_fork_create = v.OneOf(fork_choices)
561 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
562 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
562 return _DefaultPermissionsForm
563 return _DefaultPermissionsForm
563
564
564
565
565 def UserIndividualPermissionsForm(localizer):
566 def UserIndividualPermissionsForm(localizer):
566 _ = localizer
567 _ = localizer
567
568
568 class _DefaultPermissionsForm(formencode.Schema):
569 class _DefaultPermissionsForm(formencode.Schema):
569 allow_extra_fields = True
570 allow_extra_fields = True
570 filter_extra_fields = True
571 filter_extra_fields = True
571
572
572 inherit_default_permissions = v.StringBoolean(if_missing=False)
573 inherit_default_permissions = v.StringBoolean(if_missing=False)
573 return _DefaultPermissionsForm
574 return _DefaultPermissionsForm
574
575
575
576
576 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
577 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
577 _ = localizer
578 _ = localizer
578 old_data = old_data or {}
579 old_data = old_data or {}
579
580
580 class _DefaultsForm(formencode.Schema):
581 class _DefaultsForm(formencode.Schema):
581 allow_extra_fields = True
582 allow_extra_fields = True
582 filter_extra_fields = True
583 filter_extra_fields = True
583 default_repo_type = v.OneOf(supported_backends)
584 default_repo_type = v.OneOf(supported_backends)
584 default_repo_private = v.StringBoolean(if_missing=False)
585 default_repo_private = v.StringBoolean(if_missing=False)
585 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
586 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
586 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
587 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
587 default_repo_enable_locking = v.StringBoolean(if_missing=False)
588 default_repo_enable_locking = v.StringBoolean(if_missing=False)
588 return _DefaultsForm
589 return _DefaultsForm
589
590
590
591
591 def AuthSettingsForm(localizer):
592 def AuthSettingsForm(localizer):
592 _ = localizer
593 _ = localizer
593
594
594 class _AuthSettingsForm(formencode.Schema):
595 class _AuthSettingsForm(formencode.Schema):
595 allow_extra_fields = True
596 allow_extra_fields = True
596 filter_extra_fields = True
597 filter_extra_fields = True
597 auth_plugins = All(v.ValidAuthPlugins(localizer),
598 auth_plugins = All(v.ValidAuthPlugins(localizer),
598 v.UniqueListFromString(localizer)(not_empty=True))
599 v.UniqueListFromString(localizer)(not_empty=True))
599 return _AuthSettingsForm
600 return _AuthSettingsForm
600
601
601
602
602 def UserExtraEmailForm(localizer):
603 def UserExtraEmailForm(localizer):
603 _ = localizer
604 _ = localizer
604
605
605 class _UserExtraEmailForm(formencode.Schema):
606 class _UserExtraEmailForm(formencode.Schema):
606 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
607 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
607 return _UserExtraEmailForm
608 return _UserExtraEmailForm
608
609
609
610
610 def UserExtraIpForm(localizer):
611 def UserExtraIpForm(localizer):
611 _ = localizer
612 _ = localizer
612
613
613 class _UserExtraIpForm(formencode.Schema):
614 class _UserExtraIpForm(formencode.Schema):
614 ip = v.ValidIp(localizer)(not_empty=True)
615 ip = v.ValidIp(localizer)(not_empty=True)
615 return _UserExtraIpForm
616 return _UserExtraIpForm
616
617
617
618
618 def PullRequestForm(localizer, repo_id):
619 def PullRequestForm(localizer, repo_id):
619 _ = localizer
620 _ = localizer
620
621
621 class ReviewerForm(formencode.Schema):
622 class ReviewerForm(formencode.Schema):
622 user_id = v.Int(not_empty=True)
623 user_id = v.Int(not_empty=True)
623 reasons = All()
624 reasons = All()
624 rules = All(v.UniqueList(localizer, convert=int)())
625 rules = All(v.UniqueList(localizer, convert=int)())
625 mandatory = v.StringBoolean()
626 mandatory = v.StringBoolean()
626 role = v.String(if_missing='reviewer')
627 role = v.String(if_missing='reviewer')
627
628
628 class ObserverForm(formencode.Schema):
629 class ObserverForm(formencode.Schema):
629 user_id = v.Int(not_empty=True)
630 user_id = v.Int(not_empty=True)
630 reasons = All()
631 reasons = All()
631 rules = All(v.UniqueList(localizer, convert=int)())
632 rules = All(v.UniqueList(localizer, convert=int)())
632 mandatory = v.StringBoolean()
633 mandatory = v.StringBoolean()
633 role = v.String(if_missing='observer')
634 role = v.String(if_missing='observer')
634
635
635 class _PullRequestForm(formencode.Schema):
636 class _PullRequestForm(formencode.Schema):
636 allow_extra_fields = True
637 allow_extra_fields = True
637 filter_extra_fields = True
638 filter_extra_fields = True
638
639
639 common_ancestor = v.UnicodeString(strip=True, required=True)
640 common_ancestor = v.UnicodeString(strip=True, required=True)
640 source_repo = v.UnicodeString(strip=True, required=True)
641 source_repo = v.UnicodeString(strip=True, required=True)
641 source_ref = v.UnicodeString(strip=True, required=True)
642 source_ref = v.UnicodeString(strip=True, required=True)
642 target_repo = v.UnicodeString(strip=True, required=True)
643 target_repo = v.UnicodeString(strip=True, required=True)
643 target_ref = v.UnicodeString(strip=True, required=True)
644 target_ref = v.UnicodeString(strip=True, required=True)
644 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
645 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
645 v.UniqueList(localizer)(not_empty=True))
646 v.UniqueList(localizer)(not_empty=True))
646 review_members = formencode.ForEach(ReviewerForm())
647 review_members = formencode.ForEach(ReviewerForm())
647 observer_members = formencode.ForEach(ObserverForm())
648 observer_members = formencode.ForEach(ObserverForm())
648 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
649 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
649 pullrequest_desc = v.UnicodeString(strip=True, required=False)
650 pullrequest_desc = v.UnicodeString(strip=True, required=False)
650 description_renderer = v.UnicodeString(strip=True, required=False)
651 description_renderer = v.UnicodeString(strip=True, required=False)
651
652
652 return _PullRequestForm
653 return _PullRequestForm
653
654
654
655
655 def IssueTrackerPatternsForm(localizer):
656 def IssueTrackerPatternsForm(localizer):
656 _ = localizer
657 _ = localizer
657
658
658 class _IssueTrackerPatternsForm(formencode.Schema):
659 class _IssueTrackerPatternsForm(formencode.Schema):
659 allow_extra_fields = True
660 allow_extra_fields = True
660 filter_extra_fields = False
661 filter_extra_fields = False
661 chained_validators = [v.ValidPattern(localizer)]
662 chained_validators = [v.ValidPattern(localizer)]
662 return _IssueTrackerPatternsForm
663 return _IssueTrackerPatternsForm
@@ -1,2389 +1,2393
1 # Copyright (C) 2012-2023 RhodeCode GmbH
1 # Copyright (C) 2012-2023 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 """
20 """
21 pull request model for RhodeCode
21 pull request model for RhodeCode
22 """
22 """
23
23
24 import logging
24 import logging
25 import os
25 import os
26
26
27 import datetime
27 import datetime
28 import urllib.request
28 import urllib.request
29 import urllib.parse
29 import urllib.parse
30 import urllib.error
30 import urllib.error
31 import collections
31 import collections
32
32
33 import dataclasses as dataclasses
33 import dataclasses as dataclasses
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35
35
36 from rhodecode.lib.vcs.nodes import FileNode
36 from rhodecode.lib.vcs.nodes import FileNode
37 from rhodecode.translation import lazy_ugettext
37 from rhodecode.translation import lazy_ugettext
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from collections import OrderedDict
40 from collections import OrderedDict
41 from rhodecode.lib.hook_daemon.base import prepare_callback_daemon
41 from rhodecode.lib.hook_daemon.base import prepare_callback_daemon
42 from rhodecode.lib.ext_json import sjson as json
42 from rhodecode.lib.ext_json import sjson as json
43 from rhodecode.lib.markup_renderer import (
43 from rhodecode.lib.markup_renderer import (
44 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 from rhodecode.lib.hash_utils import md5_safe
45 from rhodecode.lib.hash_utils import md5_safe
46 from rhodecode.lib.str_utils import safe_str
46 from rhodecode.lib.str_utils import safe_str
47 from rhodecode.lib.utils2 import AttributeDict, get_current_rhodecode_user
47 from rhodecode.lib.utils2 import AttributeDict, get_current_rhodecode_user
48 from rhodecode.lib.vcs.backends.base import (
48 from rhodecode.lib.vcs.backends.base import (
49 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
49 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
50 TargetRefMissing, SourceRefMissing)
50 TargetRefMissing, SourceRefMissing)
51 from rhodecode.lib.vcs.conf import settings as vcs_settings
51 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.exceptions import (
52 from rhodecode.lib.vcs.exceptions import (
53 CommitDoesNotExistError, EmptyRepositoryError)
53 CommitDoesNotExistError, EmptyRepositoryError)
54 from rhodecode.model import BaseModel
54 from rhodecode.model import BaseModel
55 from rhodecode.model.changeset_status import ChangesetStatusModel
55 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.comment import CommentsModel
56 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.db import (
57 from rhodecode.model.db import (
58 aliased, null, lazyload, and_, or_, select, func, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
58 aliased, null, lazyload, and_, or_, select, func, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
59 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
60 from rhodecode.model.meta import Session
60 from rhodecode.model.meta import Session
61 from rhodecode.model.notification import NotificationModel, \
61 from rhodecode.model.notification import NotificationModel, \
62 EmailNotificationModel
62 EmailNotificationModel
63 from rhodecode.model.scm import ScmModel
63 from rhodecode.model.scm import ScmModel
64 from rhodecode.model.settings import VcsSettingsModel
64 from rhodecode.model.settings import VcsSettingsModel
65
65
66
66
67 log = logging.getLogger(__name__)
67 log = logging.getLogger(__name__)
68
68
69
69
70 # Data structure to hold the response data when updating commits during a pull
70 # Data structure to hold the response data when updating commits during a pull
71 # request update.
71 # request update.
72 class UpdateResponse(object):
72 class UpdateResponse(object):
73
73
74 def __init__(self, executed, reason, new, old, common_ancestor_id,
74 def __init__(self, executed, reason, new, old, common_ancestor_id,
75 commit_changes, source_changed, target_changed):
75 commit_changes, source_changed, target_changed):
76
76
77 self.executed = executed
77 self.executed = executed
78 self.reason = reason
78 self.reason = reason
79 self.new = new
79 self.new = new
80 self.old = old
80 self.old = old
81 self.common_ancestor_id = common_ancestor_id
81 self.common_ancestor_id = common_ancestor_id
82 self.changes = commit_changes
82 self.changes = commit_changes
83 self.source_changed = source_changed
83 self.source_changed = source_changed
84 self.target_changed = target_changed
84 self.target_changed = target_changed
85
85
86
86
87 def get_diff_info(
87 def get_diff_info(
88 source_repo, source_ref, target_repo, target_ref, get_authors=False,
88 source_repo, source_ref, target_repo, target_ref, get_authors=False,
89 get_commit_authors=True):
89 get_commit_authors=True):
90 """
90 """
91 Calculates detailed diff information for usage in preview of creation of a pull-request.
91 Calculates detailed diff information for usage in preview of creation of a pull-request.
92 This is also used for default reviewers logic
92 This is also used for default reviewers logic
93 """
93 """
94
94
95 source_scm = source_repo.scm_instance()
95 source_scm = source_repo.scm_instance()
96 target_scm = target_repo.scm_instance()
96 target_scm = target_repo.scm_instance()
97
97
98 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
98 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
99 if not ancestor_id:
99 if not ancestor_id:
100 raise ValueError(
100 raise ValueError(
101 'cannot calculate diff info without a common ancestor. '
101 'cannot calculate diff info without a common ancestor. '
102 'Make sure both repositories are related, and have a common forking commit.')
102 'Make sure both repositories are related, and have a common forking commit.')
103
103
104 # case here is that want a simple diff without incoming commits,
104 # case here is that want a simple diff without incoming commits,
105 # previewing what will be merged based only on commits in the source.
105 # previewing what will be merged based only on commits in the source.
106 log.debug('Using ancestor %s as source_ref instead of %s',
106 log.debug('Using ancestor %s as source_ref instead of %s',
107 ancestor_id, source_ref)
107 ancestor_id, source_ref)
108
108
109 # source of changes now is the common ancestor
109 # source of changes now is the common ancestor
110 source_commit = source_scm.get_commit(commit_id=ancestor_id)
110 source_commit = source_scm.get_commit(commit_id=ancestor_id)
111 # target commit becomes the source ref as it is the last commit
111 # target commit becomes the source ref as it is the last commit
112 # for diff generation this logic gives proper diff
112 # for diff generation this logic gives proper diff
113 target_commit = source_scm.get_commit(commit_id=source_ref)
113 target_commit = source_scm.get_commit(commit_id=source_ref)
114
114
115 vcs_diff = \
115 vcs_diff = \
116 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
116 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
117 ignore_whitespace=False, context=3)
117 ignore_whitespace=False, context=3)
118
118
119 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
119 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
120 diff_limit=0, file_limit=0, show_full_diff=True)
120 diff_limit=0, file_limit=0, show_full_diff=True)
121
121
122 _parsed = diff_processor.prepare()
122 _parsed = diff_processor.prepare()
123
123
124 all_files = []
124 all_files = []
125 all_files_changes = []
125 all_files_changes = []
126 changed_lines = {}
126 changed_lines = {}
127 stats = [0, 0]
127 stats = [0, 0]
128 for f in _parsed:
128 for f in _parsed:
129 all_files.append(f['filename'])
129 all_files.append(f['filename'])
130 all_files_changes.append({
130 all_files_changes.append({
131 'filename': f['filename'],
131 'filename': f['filename'],
132 'stats': f['stats']
132 'stats': f['stats']
133 })
133 })
134 stats[0] += f['stats']['added']
134 stats[0] += f['stats']['added']
135 stats[1] += f['stats']['deleted']
135 stats[1] += f['stats']['deleted']
136
136
137 changed_lines[f['filename']] = []
137 changed_lines[f['filename']] = []
138 if len(f['chunks']) < 2:
138 if len(f['chunks']) < 2:
139 continue
139 continue
140 # first line is "context" information
140 # first line is "context" information
141 for chunks in f['chunks'][1:]:
141 for chunks in f['chunks'][1:]:
142 for chunk in chunks['lines']:
142 for chunk in chunks['lines']:
143 if chunk['action'] not in ('del', 'mod'):
143 if chunk['action'] not in ('del', 'mod'):
144 continue
144 continue
145 changed_lines[f['filename']].append(chunk['old_lineno'])
145 changed_lines[f['filename']].append(chunk['old_lineno'])
146
146
147 commit_authors = []
147 commit_authors = []
148 user_counts = {}
148 user_counts = {}
149 email_counts = {}
149 email_counts = {}
150 author_counts = {}
150 author_counts = {}
151 _commit_cache = {}
151 _commit_cache = {}
152
152
153 commits = []
153 commits = []
154 if get_commit_authors:
154 if get_commit_authors:
155 log.debug('Obtaining commit authors from set of commits')
155 log.debug('Obtaining commit authors from set of commits')
156 _compare_data = target_scm.compare(
156 _compare_data = target_scm.compare(
157 target_ref, source_ref, source_scm, merge=True,
157 target_ref, source_ref, source_scm, merge=True,
158 pre_load=["author", "date", "message"]
158 pre_load=["author", "date", "message"]
159 )
159 )
160
160
161 for commit in _compare_data:
161 for commit in _compare_data:
162 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
162 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
163 # at this function which is later called via JSON serialization
163 # at this function which is later called via JSON serialization
164 serialized_commit = dict(
164 serialized_commit = dict(
165 author=commit.author,
165 author=commit.author,
166 date=commit.date,
166 date=commit.date,
167 message=commit.message,
167 message=commit.message,
168 commit_id=commit.raw_id,
168 commit_id=commit.raw_id,
169 raw_id=commit.raw_id
169 raw_id=commit.raw_id
170 )
170 )
171 commits.append(serialized_commit)
171 commits.append(serialized_commit)
172 user = User.get_from_cs_author(serialized_commit['author'])
172 user = User.get_from_cs_author(serialized_commit['author'])
173 if user and user not in commit_authors:
173 if user and user not in commit_authors:
174 commit_authors.append(user)
174 commit_authors.append(user)
175
175
176 # lines
176 # lines
177 if get_authors:
177 if get_authors:
178 log.debug('Calculating authors of changed files')
178 log.debug('Calculating authors of changed files')
179 target_commit = source_repo.get_commit(ancestor_id)
179 target_commit = source_repo.get_commit(ancestor_id)
180
180
181 for fname, lines in changed_lines.items():
181 for fname, lines in changed_lines.items():
182
182
183 try:
183 try:
184 node = target_commit.get_node(fname, pre_load=["is_binary"])
184 node = target_commit.get_node(fname, pre_load=["is_binary"])
185 except Exception:
185 except Exception:
186 log.exception("Failed to load node with path %s", fname)
186 log.exception("Failed to load node with path %s", fname)
187 continue
187 continue
188
188
189 if not isinstance(node, FileNode):
189 if not isinstance(node, FileNode):
190 continue
190 continue
191
191
192 # NOTE(marcink): for binary node we don't do annotation, just use last author
192 # NOTE(marcink): for binary node we don't do annotation, just use last author
193 if node.is_binary:
193 if node.is_binary:
194 author = node.last_commit.author
194 author = node.last_commit.author
195 email = node.last_commit.author_email
195 email = node.last_commit.author_email
196
196
197 user = User.get_from_cs_author(author)
197 user = User.get_from_cs_author(author)
198 if user:
198 if user:
199 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
199 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
200 author_counts[author] = author_counts.get(author, 0) + 1
200 author_counts[author] = author_counts.get(author, 0) + 1
201 email_counts[email] = email_counts.get(email, 0) + 1
201 email_counts[email] = email_counts.get(email, 0) + 1
202
202
203 continue
203 continue
204
204
205 for annotation in node.annotate:
205 for annotation in node.annotate:
206 line_no, commit_id, get_commit_func, line_text = annotation
206 line_no, commit_id, get_commit_func, line_text = annotation
207 if line_no in lines:
207 if line_no in lines:
208 if commit_id not in _commit_cache:
208 if commit_id not in _commit_cache:
209 _commit_cache[commit_id] = get_commit_func()
209 _commit_cache[commit_id] = get_commit_func()
210 commit = _commit_cache[commit_id]
210 commit = _commit_cache[commit_id]
211 author = commit.author
211 author = commit.author
212 email = commit.author_email
212 email = commit.author_email
213 user = User.get_from_cs_author(author)
213 user = User.get_from_cs_author(author)
214 if user:
214 if user:
215 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
215 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
216 author_counts[author] = author_counts.get(author, 0) + 1
216 author_counts[author] = author_counts.get(author, 0) + 1
217 email_counts[email] = email_counts.get(email, 0) + 1
217 email_counts[email] = email_counts.get(email, 0) + 1
218
218
219 log.debug('Default reviewers processing finished')
219 log.debug('Default reviewers processing finished')
220
220
221 return {
221 return {
222 'commits': commits,
222 'commits': commits,
223 'files': all_files_changes,
223 'files': all_files_changes,
224 'stats': stats,
224 'stats': stats,
225 'ancestor': ancestor_id,
225 'ancestor': ancestor_id,
226 # original authors of modified files
226 # original authors of modified files
227 'original_authors': {
227 'original_authors': {
228 'users': user_counts,
228 'users': user_counts,
229 'authors': author_counts,
229 'authors': author_counts,
230 'emails': email_counts,
230 'emails': email_counts,
231 },
231 },
232 'commit_authors': commit_authors
232 'commit_authors': commit_authors
233 }
233 }
234
234
235
235
236 class PullRequestModel(BaseModel):
236 class PullRequestModel(BaseModel):
237
237
238 cls = PullRequest
238 cls = PullRequest
239
239
240 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
240 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
241
241
242 UPDATE_STATUS_MESSAGES = {
242 UPDATE_STATUS_MESSAGES = {
243 UpdateFailureReason.NONE: lazy_ugettext(
243 UpdateFailureReason.NONE: lazy_ugettext(
244 'Pull request update successful.'),
244 'Pull request update successful.'),
245 UpdateFailureReason.UNKNOWN: lazy_ugettext(
245 UpdateFailureReason.UNKNOWN: lazy_ugettext(
246 'Pull request update failed because of an unknown error.'),
246 'Pull request update failed because of an unknown error.'),
247 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
247 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
248 'No update needed because the source and target have not changed.'),
248 'No update needed because the source and target have not changed.'),
249 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
249 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
250 'Pull request cannot be updated because the reference type is '
250 'Pull request cannot be updated because the reference type is '
251 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
251 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
252 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
252 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
253 'This pull request cannot be updated because the target '
253 'This pull request cannot be updated because the target '
254 'reference is missing.'),
254 'reference is missing.'),
255 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
255 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
256 'This pull request cannot be updated because the source '
256 'This pull request cannot be updated because the source '
257 'reference is missing.'),
257 'reference is missing.'),
258 }
258 }
259 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
259 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
260 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
260 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
261
261
262 def __get_pull_request(self, pull_request):
262 def __get_pull_request(self, pull_request):
263 return self._get_instance((
263 return self._get_instance((
264 PullRequest, PullRequestVersion), pull_request)
264 PullRequest, PullRequestVersion), pull_request)
265
265
266 def _check_perms(self, perms, pull_request, user, api=False):
266 def _check_perms(self, perms, pull_request, user, api=False):
267 if not api:
267 if not api:
268 return h.HasRepoPermissionAny(*perms)(
268 return h.HasRepoPermissionAny(*perms)(
269 user=user, repo_name=pull_request.target_repo.repo_name)
269 user=user, repo_name=pull_request.target_repo.repo_name)
270 else:
270 else:
271 return h.HasRepoPermissionAnyApi(*perms)(
271 return h.HasRepoPermissionAnyApi(*perms)(
272 user=user, repo_name=pull_request.target_repo.repo_name)
272 user=user, repo_name=pull_request.target_repo.repo_name)
273
273
274 def check_user_read(self, pull_request, user, api=False):
274 def check_user_read(self, pull_request, user, api=False):
275 _perms = ('repository.admin', 'repository.write', 'repository.read',)
275 _perms = ('repository.admin', 'repository.write', 'repository.read',)
276 return self._check_perms(_perms, pull_request, user, api)
276 return self._check_perms(_perms, pull_request, user, api)
277
277
278 def check_user_merge(self, pull_request, user, api=False):
278 def check_user_merge(self, pull_request, user, api=False):
279 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
279 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
280 return self._check_perms(_perms, pull_request, user, api)
280 return self._check_perms(_perms, pull_request, user, api)
281
281
282 def check_user_update(self, pull_request, user, api=False):
282 def check_user_update(self, pull_request, user, api=False):
283 owner = user.user_id == pull_request.user_id
283 owner = user.user_id == pull_request.user_id
284 return self.check_user_merge(pull_request, user, api) or owner
284 return self.check_user_merge(pull_request, user, api) or owner
285
285
286 def check_user_delete(self, pull_request, user):
286 def check_user_delete(self, pull_request, user):
287 owner = user.user_id == pull_request.user_id
287 owner = user.user_id == pull_request.user_id
288 _perms = ('repository.admin',)
288 _perms = ('repository.admin',)
289 return self._check_perms(_perms, pull_request, user) or owner
289 return self._check_perms(_perms, pull_request, user) or owner
290
290
291 def is_user_reviewer(self, pull_request, user):
291 def is_user_reviewer(self, pull_request, user):
292 return user.user_id in [
292 return user.user_id in [
293 x.user_id for x in
293 x.user_id for x in
294 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
294 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
295 if x.user
295 if x.user
296 ]
296 ]
297
297
298 def check_user_change_status(self, pull_request, user, api=False):
298 def check_user_change_status(self, pull_request, user, api=False):
299 return self.check_user_update(pull_request, user, api) \
299 return self.check_user_update(pull_request, user, api) \
300 or self.is_user_reviewer(pull_request, user)
300 or self.is_user_reviewer(pull_request, user)
301
301
302 def check_user_comment(self, pull_request, user):
302 def check_user_comment(self, pull_request, user):
303 owner = user.user_id == pull_request.user_id
303 owner = user.user_id == pull_request.user_id
304 return self.check_user_read(pull_request, user) or owner
304 return self.check_user_read(pull_request, user) or owner
305
305
306 def get(self, pull_request):
306 def get(self, pull_request):
307 return self.__get_pull_request(pull_request)
307 return self.__get_pull_request(pull_request)
308
308
309 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
309 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
310 statuses=None, opened_by=None, order_by=None,
310 statuses=None, opened_by=None, order_by=None,
311 order_dir='desc', only_created=False):
311 order_dir='desc', only_created=False):
312 repo = None
312 repo = None
313 if repo_name:
313 if repo_name:
314 repo = self._get_repo(repo_name)
314 repo = self._get_repo(repo_name)
315
315
316 q = PullRequest.query()
316 q = PullRequest.query()
317
317
318 if search_q:
318 if search_q:
319 like_expression = u'%{}%'.format(safe_str(search_q))
319 like_expression = u'%{}%'.format(safe_str(search_q))
320 q = q.join(User, User.user_id == PullRequest.user_id)
320 q = q.join(User, User.user_id == PullRequest.user_id)
321 q = q.filter(or_(
321 q = q.filter(or_(
322 cast(PullRequest.pull_request_id, String).ilike(like_expression),
322 cast(PullRequest.pull_request_id, String).ilike(like_expression),
323 User.username.ilike(like_expression),
323 User.username.ilike(like_expression),
324 PullRequest.title.ilike(like_expression),
324 PullRequest.title.ilike(like_expression),
325 PullRequest.description.ilike(like_expression),
325 PullRequest.description.ilike(like_expression),
326 ))
326 ))
327
327
328 # source or target
328 # source or target
329 if repo and source:
329 if repo and source:
330 q = q.filter(PullRequest.source_repo == repo)
330 q = q.filter(PullRequest.source_repo == repo)
331 elif repo:
331 elif repo:
332 q = q.filter(PullRequest.target_repo == repo)
332 q = q.filter(PullRequest.target_repo == repo)
333
333
334 # closed,opened
334 # closed,opened
335 if statuses:
335 if statuses:
336 q = q.filter(PullRequest.status.in_(statuses))
336 q = q.filter(PullRequest.status.in_(statuses))
337
337
338 # opened by filter
338 # opened by filter
339 if opened_by:
339 if opened_by:
340 q = q.filter(PullRequest.user_id.in_(opened_by))
340 q = q.filter(PullRequest.user_id.in_(opened_by))
341
341
342 # only get those that are in "created" state
342 # only get those that are in "created" state
343 if only_created:
343 if only_created:
344 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
344 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
345
345
346 order_map = {
346 order_map = {
347 'name_raw': PullRequest.pull_request_id,
347 'name_raw': PullRequest.pull_request_id,
348 'id': PullRequest.pull_request_id,
348 'id': PullRequest.pull_request_id,
349 'title': PullRequest.title,
349 'title': PullRequest.title,
350 'updated_on_raw': PullRequest.updated_on,
350 'updated_on_raw': PullRequest.updated_on,
351 'target_repo': PullRequest.target_repo_id
351 'target_repo': PullRequest.target_repo_id
352 }
352 }
353 if order_by and order_by in order_map:
353 if order_by and order_by in order_map:
354 if order_dir == 'asc':
354 if order_dir == 'asc':
355 q = q.order_by(order_map[order_by].asc())
355 q = q.order_by(order_map[order_by].asc())
356 else:
356 else:
357 q = q.order_by(order_map[order_by].desc())
357 q = q.order_by(order_map[order_by].desc())
358
358
359 return q
359 return q
360
360
361 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
361 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
362 opened_by=None):
362 opened_by=None):
363 """
363 """
364 Count the number of pull requests for a specific repository.
364 Count the number of pull requests for a specific repository.
365
365
366 :param repo_name: target or source repo
366 :param repo_name: target or source repo
367 :param search_q: filter by text
367 :param search_q: filter by text
368 :param source: boolean flag to specify if repo_name refers to source
368 :param source: boolean flag to specify if repo_name refers to source
369 :param statuses: list of pull request statuses
369 :param statuses: list of pull request statuses
370 :param opened_by: author user of the pull request
370 :param opened_by: author user of the pull request
371 :returns: int number of pull requests
371 :returns: int number of pull requests
372 """
372 """
373 q = self._prepare_get_all_query(
373 q = self._prepare_get_all_query(
374 repo_name, search_q=search_q, source=source, statuses=statuses,
374 repo_name, search_q=search_q, source=source, statuses=statuses,
375 opened_by=opened_by)
375 opened_by=opened_by)
376
376
377 return q.count()
377 return q.count()
378
378
379 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
379 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
380 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
380 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
381 """
381 """
382 Get all pull requests for a specific repository.
382 Get all pull requests for a specific repository.
383
383
384 :param repo_name: target or source repo
384 :param repo_name: target or source repo
385 :param search_q: filter by text
385 :param search_q: filter by text
386 :param source: boolean flag to specify if repo_name refers to source
386 :param source: boolean flag to specify if repo_name refers to source
387 :param statuses: list of pull request statuses
387 :param statuses: list of pull request statuses
388 :param opened_by: author user of the pull request
388 :param opened_by: author user of the pull request
389 :param offset: pagination offset
389 :param offset: pagination offset
390 :param length: length of returned list
390 :param length: length of returned list
391 :param order_by: order of the returned list
391 :param order_by: order of the returned list
392 :param order_dir: 'asc' or 'desc' ordering direction
392 :param order_dir: 'asc' or 'desc' ordering direction
393 :returns: list of pull requests
393 :returns: list of pull requests
394 """
394 """
395 q = self._prepare_get_all_query(
395 q = self._prepare_get_all_query(
396 repo_name, search_q=search_q, source=source, statuses=statuses,
396 repo_name, search_q=search_q, source=source, statuses=statuses,
397 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
397 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
398
398
399 if length:
399 if length:
400 pull_requests = q.limit(length).offset(offset).all()
400 pull_requests = q.limit(length).offset(offset).all()
401 else:
401 else:
402 pull_requests = q.all()
402 pull_requests = q.all()
403
403
404 return pull_requests
404 return pull_requests
405
405
406 def count_awaiting_review(self, repo_name, search_q=None, statuses=None):
406 def count_awaiting_review(self, repo_name, search_q=None, statuses=None):
407 """
407 """
408 Count the number of pull requests for a specific repository that are
408 Count the number of pull requests for a specific repository that are
409 awaiting review.
409 awaiting review.
410
410
411 :param repo_name: target or source repo
411 :param repo_name: target or source repo
412 :param search_q: filter by text
412 :param search_q: filter by text
413 :param statuses: list of pull request statuses
413 :param statuses: list of pull request statuses
414 :returns: int number of pull requests
414 :returns: int number of pull requests
415 """
415 """
416 pull_requests = self.get_awaiting_review(
416 pull_requests = self.get_awaiting_review(
417 repo_name, search_q=search_q, statuses=statuses)
417 repo_name, search_q=search_q, statuses=statuses)
418
418
419 return len(pull_requests)
419 return len(pull_requests)
420
420
421 def get_awaiting_review(self, repo_name, search_q=None, statuses=None,
421 def get_awaiting_review(self, repo_name, search_q=None, statuses=None,
422 offset=0, length=None, order_by=None, order_dir='desc'):
422 offset=0, length=None, order_by=None, order_dir='desc'):
423 """
423 """
424 Get all pull requests for a specific repository that are awaiting
424 Get all pull requests for a specific repository that are awaiting
425 review.
425 review.
426
426
427 :param repo_name: target or source repo
427 :param repo_name: target or source repo
428 :param search_q: filter by text
428 :param search_q: filter by text
429 :param statuses: list of pull request statuses
429 :param statuses: list of pull request statuses
430 :param offset: pagination offset
430 :param offset: pagination offset
431 :param length: length of returned list
431 :param length: length of returned list
432 :param order_by: order of the returned list
432 :param order_by: order of the returned list
433 :param order_dir: 'asc' or 'desc' ordering direction
433 :param order_dir: 'asc' or 'desc' ordering direction
434 :returns: list of pull requests
434 :returns: list of pull requests
435 """
435 """
436 pull_requests = self.get_all(
436 pull_requests = self.get_all(
437 repo_name, search_q=search_q, statuses=statuses,
437 repo_name, search_q=search_q, statuses=statuses,
438 order_by=order_by, order_dir=order_dir)
438 order_by=order_by, order_dir=order_dir)
439
439
440 _filtered_pull_requests = []
440 _filtered_pull_requests = []
441 for pr in pull_requests:
441 for pr in pull_requests:
442 status = pr.calculated_review_status()
442 status = pr.calculated_review_status()
443 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
443 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
444 ChangesetStatus.STATUS_UNDER_REVIEW]:
444 ChangesetStatus.STATUS_UNDER_REVIEW]:
445 _filtered_pull_requests.append(pr)
445 _filtered_pull_requests.append(pr)
446 if length:
446 if length:
447 return _filtered_pull_requests[offset:offset+length]
447 return _filtered_pull_requests[offset:offset+length]
448 else:
448 else:
449 return _filtered_pull_requests
449 return _filtered_pull_requests
450
450
451 def _prepare_awaiting_my_review_review_query(
451 def _prepare_awaiting_my_review_review_query(
452 self, repo_name, user_id, search_q=None, statuses=None,
452 self, repo_name, user_id, search_q=None, statuses=None,
453 order_by=None, order_dir='desc'):
453 order_by=None, order_dir='desc'):
454
454
455 for_review_statuses = [
455 for_review_statuses = [
456 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
456 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
457 ]
457 ]
458
458
459 pull_request_alias = aliased(PullRequest)
459 pull_request_alias = aliased(PullRequest)
460 status_alias = aliased(ChangesetStatus)
460 status_alias = aliased(ChangesetStatus)
461 reviewers_alias = aliased(PullRequestReviewers)
461 reviewers_alias = aliased(PullRequestReviewers)
462 repo_alias = aliased(Repository)
462 repo_alias = aliased(Repository)
463
463
464 last_ver_subq = Session()\
464 last_ver_subq = Session()\
465 .query(func.min(ChangesetStatus.version)) \
465 .query(func.min(ChangesetStatus.version)) \
466 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
466 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
467 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
467 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
468 .subquery()
468 .subquery()
469
469
470 q = Session().query(pull_request_alias) \
470 q = Session().query(pull_request_alias) \
471 .options(lazyload(pull_request_alias.author)) \
471 .options(lazyload(pull_request_alias.author)) \
472 .join(reviewers_alias,
472 .join(reviewers_alias,
473 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
473 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
474 .join(repo_alias,
474 .join(repo_alias,
475 repo_alias.repo_id == pull_request_alias.target_repo_id) \
475 repo_alias.repo_id == pull_request_alias.target_repo_id) \
476 .outerjoin(status_alias,
476 .outerjoin(status_alias,
477 and_(status_alias.user_id == reviewers_alias.user_id,
477 and_(status_alias.user_id == reviewers_alias.user_id,
478 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
478 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
479 .filter(or_(status_alias.version == null(),
479 .filter(or_(status_alias.version == null(),
480 status_alias.version == last_ver_subq)) \
480 status_alias.version == last_ver_subq)) \
481 .filter(reviewers_alias.user_id == user_id) \
481 .filter(reviewers_alias.user_id == user_id) \
482 .filter(repo_alias.repo_name == repo_name) \
482 .filter(repo_alias.repo_name == repo_name) \
483 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
483 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
484 .group_by(pull_request_alias)
484 .group_by(pull_request_alias)
485
485
486 # closed,opened
486 # closed,opened
487 if statuses:
487 if statuses:
488 q = q.filter(pull_request_alias.status.in_(statuses))
488 q = q.filter(pull_request_alias.status.in_(statuses))
489
489
490 if search_q:
490 if search_q:
491 like_expression = u'%{}%'.format(safe_str(search_q))
491 like_expression = u'%{}%'.format(safe_str(search_q))
492 q = q.join(User, User.user_id == pull_request_alias.user_id)
492 q = q.join(User, User.user_id == pull_request_alias.user_id)
493 q = q.filter(or_(
493 q = q.filter(or_(
494 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
494 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
495 User.username.ilike(like_expression),
495 User.username.ilike(like_expression),
496 pull_request_alias.title.ilike(like_expression),
496 pull_request_alias.title.ilike(like_expression),
497 pull_request_alias.description.ilike(like_expression),
497 pull_request_alias.description.ilike(like_expression),
498 ))
498 ))
499
499
500 order_map = {
500 order_map = {
501 'name_raw': pull_request_alias.pull_request_id,
501 'name_raw': pull_request_alias.pull_request_id,
502 'title': pull_request_alias.title,
502 'title': pull_request_alias.title,
503 'updated_on_raw': pull_request_alias.updated_on,
503 'updated_on_raw': pull_request_alias.updated_on,
504 'target_repo': pull_request_alias.target_repo_id
504 'target_repo': pull_request_alias.target_repo_id
505 }
505 }
506 if order_by and order_by in order_map:
506 if order_by and order_by in order_map:
507 if order_dir == 'asc':
507 if order_dir == 'asc':
508 q = q.order_by(order_map[order_by].asc())
508 q = q.order_by(order_map[order_by].asc())
509 else:
509 else:
510 q = q.order_by(order_map[order_by].desc())
510 q = q.order_by(order_map[order_by].desc())
511
511
512 return q
512 return q
513
513
514 def count_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None):
514 def count_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None):
515 """
515 """
516 Count the number of pull requests for a specific repository that are
516 Count the number of pull requests for a specific repository that are
517 awaiting review from a specific user.
517 awaiting review from a specific user.
518
518
519 :param repo_name: target or source repo
519 :param repo_name: target or source repo
520 :param user_id: reviewer user of the pull request
520 :param user_id: reviewer user of the pull request
521 :param search_q: filter by text
521 :param search_q: filter by text
522 :param statuses: list of pull request statuses
522 :param statuses: list of pull request statuses
523 :returns: int number of pull requests
523 :returns: int number of pull requests
524 """
524 """
525 q = self._prepare_awaiting_my_review_review_query(
525 q = self._prepare_awaiting_my_review_review_query(
526 repo_name, user_id, search_q=search_q, statuses=statuses)
526 repo_name, user_id, search_q=search_q, statuses=statuses)
527 return q.count()
527 return q.count()
528
528
529 def get_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None,
529 def get_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None,
530 offset=0, length=None, order_by=None, order_dir='desc'):
530 offset=0, length=None, order_by=None, order_dir='desc'):
531 """
531 """
532 Get all pull requests for a specific repository that are awaiting
532 Get all pull requests for a specific repository that are awaiting
533 review from a specific user.
533 review from a specific user.
534
534
535 :param repo_name: target or source repo
535 :param repo_name: target or source repo
536 :param user_id: reviewer user of the pull request
536 :param user_id: reviewer user of the pull request
537 :param search_q: filter by text
537 :param search_q: filter by text
538 :param statuses: list of pull request statuses
538 :param statuses: list of pull request statuses
539 :param offset: pagination offset
539 :param offset: pagination offset
540 :param length: length of returned list
540 :param length: length of returned list
541 :param order_by: order of the returned list
541 :param order_by: order of the returned list
542 :param order_dir: 'asc' or 'desc' ordering direction
542 :param order_dir: 'asc' or 'desc' ordering direction
543 :returns: list of pull requests
543 :returns: list of pull requests
544 """
544 """
545
545
546 q = self._prepare_awaiting_my_review_review_query(
546 q = self._prepare_awaiting_my_review_review_query(
547 repo_name, user_id, search_q=search_q, statuses=statuses,
547 repo_name, user_id, search_q=search_q, statuses=statuses,
548 order_by=order_by, order_dir=order_dir)
548 order_by=order_by, order_dir=order_dir)
549
549
550 if length:
550 if length:
551 pull_requests = q.limit(length).offset(offset).all()
551 pull_requests = q.limit(length).offset(offset).all()
552 else:
552 else:
553 pull_requests = q.all()
553 pull_requests = q.all()
554
554
555 return pull_requests
555 return pull_requests
556
556
557 def _prepare_im_participating_query(self, user_id=None, statuses=None, query='',
557 def _prepare_im_participating_query(self, user_id=None, statuses=None, query='',
558 order_by=None, order_dir='desc'):
558 order_by=None, order_dir='desc'):
559 """
559 """
560 return a query of pull-requests user is an creator, or he's added as a reviewer
560 return a query of pull-requests user is an creator, or he's added as a reviewer
561 """
561 """
562 q = PullRequest.query()
562 q = PullRequest.query()
563 if user_id:
563 if user_id:
564
564
565 base_query = select(PullRequestReviewers)\
565 base_query = select(PullRequestReviewers)\
566 .where(PullRequestReviewers.user_id == user_id)\
566 .where(PullRequestReviewers.user_id == user_id)\
567 .with_only_columns(PullRequestReviewers.pull_request_id)
567 .with_only_columns(PullRequestReviewers.pull_request_id)
568
568
569 user_filter = or_(
569 user_filter = or_(
570 PullRequest.user_id == user_id,
570 PullRequest.user_id == user_id,
571 PullRequest.pull_request_id.in_(base_query)
571 PullRequest.pull_request_id.in_(base_query)
572 )
572 )
573 q = PullRequest.query().filter(user_filter)
573 q = PullRequest.query().filter(user_filter)
574
574
575 # closed,opened
575 # closed,opened
576 if statuses:
576 if statuses:
577 q = q.filter(PullRequest.status.in_(statuses))
577 q = q.filter(PullRequest.status.in_(statuses))
578
578
579 if query:
579 if query:
580 like_expression = u'%{}%'.format(safe_str(query))
580 like_expression = u'%{}%'.format(safe_str(query))
581 q = q.join(User, User.user_id == PullRequest.user_id)
581 q = q.join(User, User.user_id == PullRequest.user_id)
582 q = q.filter(or_(
582 q = q.filter(or_(
583 cast(PullRequest.pull_request_id, String).ilike(like_expression),
583 cast(PullRequest.pull_request_id, String).ilike(like_expression),
584 User.username.ilike(like_expression),
584 User.username.ilike(like_expression),
585 PullRequest.title.ilike(like_expression),
585 PullRequest.title.ilike(like_expression),
586 PullRequest.description.ilike(like_expression),
586 PullRequest.description.ilike(like_expression),
587 ))
587 ))
588
588
589 order_map = {
589 order_map = {
590 'name_raw': PullRequest.pull_request_id,
590 'name_raw': PullRequest.pull_request_id,
591 'title': PullRequest.title,
591 'title': PullRequest.title,
592 'updated_on_raw': PullRequest.updated_on,
592 'updated_on_raw': PullRequest.updated_on,
593 'target_repo': PullRequest.target_repo_id
593 'target_repo': PullRequest.target_repo_id
594 }
594 }
595 if order_by and order_by in order_map:
595 if order_by and order_by in order_map:
596 if order_dir == 'asc':
596 if order_dir == 'asc':
597 q = q.order_by(order_map[order_by].asc())
597 q = q.order_by(order_map[order_by].asc())
598 else:
598 else:
599 q = q.order_by(order_map[order_by].desc())
599 q = q.order_by(order_map[order_by].desc())
600
600
601 return q
601 return q
602
602
603 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
603 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
604 q = self._prepare_im_participating_query(user_id, statuses=statuses, query=query)
604 q = self._prepare_im_participating_query(user_id, statuses=statuses, query=query)
605 return q.count()
605 return q.count()
606
606
607 def get_im_participating_in(
607 def get_im_participating_in(
608 self, user_id=None, statuses=None, query='', offset=0,
608 self, user_id=None, statuses=None, query='', offset=0,
609 length=None, order_by=None, order_dir='desc'):
609 length=None, order_by=None, order_dir='desc'):
610 """
610 """
611 Get all Pull requests that i'm participating in as a reviewer, or i have opened
611 Get all Pull requests that i'm participating in as a reviewer, or i have opened
612 """
612 """
613
613
614 q = self._prepare_im_participating_query(
614 q = self._prepare_im_participating_query(
615 user_id, statuses=statuses, query=query, order_by=order_by,
615 user_id, statuses=statuses, query=query, order_by=order_by,
616 order_dir=order_dir)
616 order_dir=order_dir)
617
617
618 if length:
618 if length:
619 pull_requests = q.limit(length).offset(offset).all()
619 pull_requests = q.limit(length).offset(offset).all()
620 else:
620 else:
621 pull_requests = q.all()
621 pull_requests = q.all()
622
622
623 return pull_requests
623 return pull_requests
624
624
625 def _prepare_participating_in_for_review_query(
625 def _prepare_participating_in_for_review_query(
626 self, user_id, statuses=None, query='', order_by=None, order_dir='desc'):
626 self, user_id, statuses=None, query='', order_by=None, order_dir='desc'):
627
627
628 for_review_statuses = [
628 for_review_statuses = [
629 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
629 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
630 ]
630 ]
631
631
632 pull_request_alias = aliased(PullRequest)
632 pull_request_alias = aliased(PullRequest)
633 status_alias = aliased(ChangesetStatus)
633 status_alias = aliased(ChangesetStatus)
634 reviewers_alias = aliased(PullRequestReviewers)
634 reviewers_alias = aliased(PullRequestReviewers)
635
635
636 last_ver_subq = Session()\
636 last_ver_subq = Session()\
637 .query(func.min(ChangesetStatus.version)) \
637 .query(func.min(ChangesetStatus.version)) \
638 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
638 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
639 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
639 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
640 .subquery()
640 .subquery()
641
641
642 q = Session().query(pull_request_alias) \
642 q = Session().query(pull_request_alias) \
643 .options(lazyload(pull_request_alias.author)) \
643 .options(lazyload(pull_request_alias.author)) \
644 .join(reviewers_alias,
644 .join(reviewers_alias,
645 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
645 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
646 .outerjoin(status_alias,
646 .outerjoin(status_alias,
647 and_(status_alias.user_id == reviewers_alias.user_id,
647 and_(status_alias.user_id == reviewers_alias.user_id,
648 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
648 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
649 .filter(or_(status_alias.version == null(),
649 .filter(or_(status_alias.version == null(),
650 status_alias.version == last_ver_subq)) \
650 status_alias.version == last_ver_subq)) \
651 .filter(reviewers_alias.user_id == user_id) \
651 .filter(reviewers_alias.user_id == user_id) \
652 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
652 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
653 .group_by(pull_request_alias)
653 .group_by(pull_request_alias)
654
654
655 # closed,opened
655 # closed,opened
656 if statuses:
656 if statuses:
657 q = q.filter(pull_request_alias.status.in_(statuses))
657 q = q.filter(pull_request_alias.status.in_(statuses))
658
658
659 if query:
659 if query:
660 like_expression = u'%{}%'.format(safe_str(query))
660 like_expression = u'%{}%'.format(safe_str(query))
661 q = q.join(User, User.user_id == pull_request_alias.user_id)
661 q = q.join(User, User.user_id == pull_request_alias.user_id)
662 q = q.filter(or_(
662 q = q.filter(or_(
663 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
663 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
664 User.username.ilike(like_expression),
664 User.username.ilike(like_expression),
665 pull_request_alias.title.ilike(like_expression),
665 pull_request_alias.title.ilike(like_expression),
666 pull_request_alias.description.ilike(like_expression),
666 pull_request_alias.description.ilike(like_expression),
667 ))
667 ))
668
668
669 order_map = {
669 order_map = {
670 'name_raw': pull_request_alias.pull_request_id,
670 'name_raw': pull_request_alias.pull_request_id,
671 'title': pull_request_alias.title,
671 'title': pull_request_alias.title,
672 'updated_on_raw': pull_request_alias.updated_on,
672 'updated_on_raw': pull_request_alias.updated_on,
673 'target_repo': pull_request_alias.target_repo_id
673 'target_repo': pull_request_alias.target_repo_id
674 }
674 }
675 if order_by and order_by in order_map:
675 if order_by and order_by in order_map:
676 if order_dir == 'asc':
676 if order_dir == 'asc':
677 q = q.order_by(order_map[order_by].asc())
677 q = q.order_by(order_map[order_by].asc())
678 else:
678 else:
679 q = q.order_by(order_map[order_by].desc())
679 q = q.order_by(order_map[order_by].desc())
680
680
681 return q
681 return q
682
682
683 def count_im_participating_in_for_review(self, user_id, statuses=None, query=''):
683 def count_im_participating_in_for_review(self, user_id, statuses=None, query=''):
684 q = self._prepare_participating_in_for_review_query(user_id, statuses=statuses, query=query)
684 q = self._prepare_participating_in_for_review_query(user_id, statuses=statuses, query=query)
685 return q.count()
685 return q.count()
686
686
687 def get_im_participating_in_for_review(
687 def get_im_participating_in_for_review(
688 self, user_id, statuses=None, query='', offset=0,
688 self, user_id, statuses=None, query='', offset=0,
689 length=None, order_by=None, order_dir='desc'):
689 length=None, order_by=None, order_dir='desc'):
690 """
690 """
691 Get all Pull requests that needs user approval or rejection
691 Get all Pull requests that needs user approval or rejection
692 """
692 """
693
693
694 q = self._prepare_participating_in_for_review_query(
694 q = self._prepare_participating_in_for_review_query(
695 user_id, statuses=statuses, query=query, order_by=order_by,
695 user_id, statuses=statuses, query=query, order_by=order_by,
696 order_dir=order_dir)
696 order_dir=order_dir)
697
697
698 if length:
698 if length:
699 pull_requests = q.limit(length).offset(offset).all()
699 pull_requests = q.limit(length).offset(offset).all()
700 else:
700 else:
701 pull_requests = q.all()
701 pull_requests = q.all()
702
702
703 return pull_requests
703 return pull_requests
704
704
705 def get_versions(self, pull_request):
705 def get_versions(self, pull_request):
706 """
706 """
707 returns version of pull request sorted by ID descending
707 returns version of pull request sorted by ID descending
708 """
708 """
709 return PullRequestVersion.query()\
709 return PullRequestVersion.query()\
710 .filter(PullRequestVersion.pull_request == pull_request)\
710 .filter(PullRequestVersion.pull_request == pull_request)\
711 .order_by(PullRequestVersion.pull_request_version_id.asc())\
711 .order_by(PullRequestVersion.pull_request_version_id.asc())\
712 .all()
712 .all()
713
713
714 def get_pr_version(self, pull_request_id, version=None):
714 def get_pr_version(self, pull_request_id, version=None):
715 at_version = None
715 at_version = None
716
716
717 if version and version == 'latest':
717 if version and version == 'latest':
718 pull_request_ver = PullRequest.get(pull_request_id)
718 pull_request_ver = PullRequest.get(pull_request_id)
719 pull_request_obj = pull_request_ver
719 pull_request_obj = pull_request_ver
720 _org_pull_request_obj = pull_request_obj
720 _org_pull_request_obj = pull_request_obj
721 at_version = 'latest'
721 at_version = 'latest'
722 elif version:
722 elif version:
723 pull_request_ver = PullRequestVersion.get_or_404(version)
723 pull_request_ver = PullRequestVersion.get_or_404(version)
724 pull_request_obj = pull_request_ver
724 pull_request_obj = pull_request_ver
725 _org_pull_request_obj = pull_request_ver.pull_request
725 _org_pull_request_obj = pull_request_ver.pull_request
726 at_version = pull_request_ver.pull_request_version_id
726 at_version = pull_request_ver.pull_request_version_id
727 else:
727 else:
728 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
728 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
729 pull_request_id)
729 pull_request_id)
730
730
731 pull_request_display_obj = PullRequest.get_pr_display_object(
731 pull_request_display_obj = PullRequest.get_pr_display_object(
732 pull_request_obj, _org_pull_request_obj)
732 pull_request_obj, _org_pull_request_obj)
733
733
734 return _org_pull_request_obj, pull_request_obj, \
734 return _org_pull_request_obj, pull_request_obj, \
735 pull_request_display_obj, at_version
735 pull_request_display_obj, at_version
736
736
737 def pr_commits_versions(self, versions):
737 def pr_commits_versions(self, versions):
738 """
738 """
739 Maps the pull-request commits into all known PR versions. This way we can obtain
739 Maps the pull-request commits into all known PR versions. This way we can obtain
740 each pr version the commit was introduced in.
740 each pr version the commit was introduced in.
741 """
741 """
742 commit_versions = collections.defaultdict(list)
742 commit_versions = collections.defaultdict(list)
743 num_versions = [x.pull_request_version_id for x in versions]
743 num_versions = [x.pull_request_version_id for x in versions]
744 for ver in versions:
744 for ver in versions:
745 for commit_id in ver.revisions:
745 for commit_id in ver.revisions:
746 ver_idx = ChangesetComment.get_index_from_version(
746 ver_idx = ChangesetComment.get_index_from_version(
747 ver.pull_request_version_id, num_versions=num_versions)
747 ver.pull_request_version_id, num_versions=num_versions)
748 commit_versions[commit_id].append(ver_idx)
748 commit_versions[commit_id].append(ver_idx)
749 return commit_versions
749 return commit_versions
750
750
751 def create(self, created_by, source_repo, source_ref, target_repo,
751 def create(self, created_by, source_repo, source_ref, target_repo,
752 target_ref, revisions, reviewers, observers, title, description=None,
752 target_ref, revisions, reviewers, observers, title, description=None,
753 common_ancestor_id=None,
753 common_ancestor_id=None,
754 description_renderer=None,
754 description_renderer=None,
755 reviewer_data=None, translator=None, auth_user=None):
755 reviewer_data=None, translator=None, auth_user=None):
756 translator = translator or get_current_request().translate
756 translator = translator or get_current_request().translate
757
757
758 created_by_user = self._get_user(created_by)
758 created_by_user = self._get_user(created_by)
759 auth_user = auth_user or created_by_user.AuthUser()
759 auth_user = auth_user or created_by_user.AuthUser()
760 source_repo = self._get_repo(source_repo)
760 source_repo = self._get_repo(source_repo)
761 target_repo = self._get_repo(target_repo)
761 target_repo = self._get_repo(target_repo)
762
762
763 pull_request = PullRequest()
763 pull_request = PullRequest()
764 pull_request.source_repo = source_repo
764 pull_request.source_repo = source_repo
765 pull_request.source_ref = source_ref
765 pull_request.source_ref = source_ref
766 pull_request.target_repo = target_repo
766 pull_request.target_repo = target_repo
767 pull_request.target_ref = target_ref
767 pull_request.target_ref = target_ref
768 pull_request.revisions = revisions
768 pull_request.revisions = revisions
769 pull_request.title = title
769 pull_request.title = title
770 pull_request.description = description
770 pull_request.description = description
771 pull_request.description_renderer = description_renderer
771 pull_request.description_renderer = description_renderer
772 pull_request.author = created_by_user
772 pull_request.author = created_by_user
773 pull_request.reviewer_data = reviewer_data
773 pull_request.reviewer_data = reviewer_data
774 pull_request.pull_request_state = pull_request.STATE_CREATING
774 pull_request.pull_request_state = pull_request.STATE_CREATING
775 pull_request.common_ancestor_id = common_ancestor_id
775 pull_request.common_ancestor_id = common_ancestor_id
776
776
777 Session().add(pull_request)
777 Session().add(pull_request)
778 Session().flush()
778 Session().flush()
779
779
780 reviewer_ids = set()
780 reviewer_ids = set()
781 # members / reviewers
781 # members / reviewers
782 for reviewer_object in reviewers:
782 for reviewer_object in reviewers:
783 user_id, reasons, mandatory, role, rules = reviewer_object
783 user_id, reasons, mandatory, role, rules = reviewer_object
784 user = self._get_user(user_id)
784 user = self._get_user(user_id)
785
785
786 # skip duplicates
786 # skip duplicates
787 if user.user_id in reviewer_ids:
787 if user.user_id in reviewer_ids:
788 continue
788 continue
789
789
790 reviewer_ids.add(user.user_id)
790 reviewer_ids.add(user.user_id)
791
791
792 reviewer = PullRequestReviewers()
792 reviewer = PullRequestReviewers()
793 reviewer.user = user
793 reviewer.user = user
794 reviewer.pull_request = pull_request
794 reviewer.pull_request = pull_request
795 reviewer.reasons = reasons
795 reviewer.reasons = reasons
796 reviewer.mandatory = mandatory
796 reviewer.mandatory = mandatory
797 reviewer.role = role
797 reviewer.role = role
798
798
799 # NOTE(marcink): pick only first rule for now
799 # NOTE(marcink): pick only first rule for now
800 rule_id = list(rules)[0] if rules else None
800 rule_id = list(rules)[0] if rules else None
801 rule = RepoReviewRule.get(rule_id) if rule_id else None
801 rule = RepoReviewRule.get(rule_id) if rule_id else None
802 if rule:
802 if rule:
803 review_group = rule.user_group_vote_rule(user_id)
803 review_group = rule.user_group_vote_rule(user_id)
804 # we check if this particular reviewer is member of a voting group
804 # we check if this particular reviewer is member of a voting group
805 if review_group:
805 if review_group:
806 # NOTE(marcink):
806 # NOTE(marcink):
807 # can be that user is member of more but we pick the first same,
807 # can be that user is member of more but we pick the first same,
808 # same as default reviewers algo
808 # same as default reviewers algo
809 review_group = review_group[0]
809 review_group = review_group[0]
810
810
811 rule_data = {
811 rule_data = {
812 'rule_name':
812 'rule_name':
813 rule.review_rule_name,
813 rule.review_rule_name,
814 'rule_user_group_entry_id':
814 'rule_user_group_entry_id':
815 review_group.repo_review_rule_users_group_id,
815 review_group.repo_review_rule_users_group_id,
816 'rule_user_group_name':
816 'rule_user_group_name':
817 review_group.users_group.users_group_name,
817 review_group.users_group.users_group_name,
818 'rule_user_group_members':
818 'rule_user_group_members':
819 [x.user.username for x in review_group.users_group.members],
819 [x.user.username for x in review_group.users_group.members],
820 'rule_user_group_members_id':
820 'rule_user_group_members_id':
821 [x.user.user_id for x in review_group.users_group.members],
821 [x.user.user_id for x in review_group.users_group.members],
822 }
822 }
823 # e.g {'vote_rule': -1, 'mandatory': True}
823 # e.g {'vote_rule': -1, 'mandatory': True}
824 rule_data.update(review_group.rule_data())
824 rule_data.update(review_group.rule_data())
825
825
826 reviewer.rule_data = rule_data
826 reviewer.rule_data = rule_data
827
827
828 Session().add(reviewer)
828 Session().add(reviewer)
829 Session().flush()
829 Session().flush()
830
830
831 for observer_object in observers:
831 for observer_object in observers:
832 user_id, reasons, mandatory, role, rules = observer_object
832 user_id, reasons, mandatory, role, rules = observer_object
833 user = self._get_user(user_id)
833 user = self._get_user(user_id)
834
834
835 # skip duplicates from reviewers
835 # skip duplicates from reviewers
836 if user.user_id in reviewer_ids:
836 if user.user_id in reviewer_ids:
837 continue
837 continue
838
838
839 #reviewer_ids.add(user.user_id)
839 #reviewer_ids.add(user.user_id)
840
840
841 observer = PullRequestReviewers()
841 observer = PullRequestReviewers()
842 observer.user = user
842 observer.user = user
843 observer.pull_request = pull_request
843 observer.pull_request = pull_request
844 observer.reasons = reasons
844 observer.reasons = reasons
845 observer.mandatory = mandatory
845 observer.mandatory = mandatory
846 observer.role = role
846 observer.role = role
847
847
848 # NOTE(marcink): pick only first rule for now
848 # NOTE(marcink): pick only first rule for now
849 rule_id = list(rules)[0] if rules else None
849 rule_id = list(rules)[0] if rules else None
850 rule = RepoReviewRule.get(rule_id) if rule_id else None
850 rule = RepoReviewRule.get(rule_id) if rule_id else None
851 if rule:
851 if rule:
852 # TODO(marcink): do we need this for observers ??
852 # TODO(marcink): do we need this for observers ??
853 pass
853 pass
854
854
855 Session().add(observer)
855 Session().add(observer)
856 Session().flush()
856 Session().flush()
857
857
858 # Set approval status to "Under Review" for all commits which are
858 # Set approval status to "Under Review" for all commits which are
859 # part of this pull request.
859 # part of this pull request.
860 ChangesetStatusModel().set_status(
860 ChangesetStatusModel().set_status(
861 repo=target_repo,
861 repo=target_repo,
862 status=ChangesetStatus.STATUS_UNDER_REVIEW,
862 status=ChangesetStatus.STATUS_UNDER_REVIEW,
863 user=created_by_user,
863 user=created_by_user,
864 pull_request=pull_request
864 pull_request=pull_request
865 )
865 )
866 # we commit early at this point. This has to do with a fact
866 # we commit early at this point. This has to do with a fact
867 # that before queries do some row-locking. And because of that
867 # that before queries do some row-locking. And because of that
868 # we need to commit and finish transaction before below validate call
868 # we need to commit and finish transaction before below validate call
869 # that for large repos could be long resulting in long row locks
869 # that for large repos could be long resulting in long row locks
870 Session().commit()
870 Session().commit()
871
871
872 # prepare workspace, and run initial merge simulation. Set state during that
872 # prepare workspace, and run initial merge simulation. Set state during that
873 # operation
873 # operation
874 pull_request = PullRequest.get(pull_request.pull_request_id)
874 pull_request = PullRequest.get(pull_request.pull_request_id)
875
875
876 # set as merging, for merge simulation, and if finished to created so we mark
876 # set as merging, for merge simulation, and if finished to created so we mark
877 # simulation is working fine
877 # simulation is working fine
878 with pull_request.set_state(PullRequest.STATE_MERGING,
878 with pull_request.set_state(PullRequest.STATE_MERGING,
879 final_state=PullRequest.STATE_CREATED) as state_obj:
879 final_state=PullRequest.STATE_CREATED) as state_obj:
880 MergeCheck.validate(
880 MergeCheck.validate(
881 pull_request, auth_user=auth_user, translator=translator)
881 pull_request, auth_user=auth_user, translator=translator)
882
882
883 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
883 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
884 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
884 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
885
885
886 creation_data = pull_request.get_api_data(with_merge_state=False)
886 creation_data = pull_request.get_api_data(with_merge_state=False)
887 self._log_audit_action(
887 self._log_audit_action(
888 'repo.pull_request.create', {'data': creation_data},
888 'repo.pull_request.create', {'data': creation_data},
889 auth_user, pull_request)
889 auth_user, pull_request)
890
890
891 return pull_request
891 return pull_request
892
892
893 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
893 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
894 pull_request = self.__get_pull_request(pull_request)
894 pull_request = self.__get_pull_request(pull_request)
895 target_scm = pull_request.target_repo.scm_instance()
895 target_scm = pull_request.target_repo.scm_instance()
896 if action == 'create':
896 if action == 'create':
897 trigger_hook = hooks_utils.trigger_create_pull_request_hook
897 trigger_hook = hooks_utils.trigger_create_pull_request_hook
898 elif action == 'merge':
898 elif action == 'merge':
899 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
899 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
900 elif action == 'close':
900 elif action == 'close':
901 trigger_hook = hooks_utils.trigger_close_pull_request_hook
901 trigger_hook = hooks_utils.trigger_close_pull_request_hook
902 elif action == 'review_status_change':
902 elif action == 'review_status_change':
903 trigger_hook = hooks_utils.trigger_review_pull_request_hook
903 trigger_hook = hooks_utils.trigger_review_pull_request_hook
904 elif action == 'update':
904 elif action == 'update':
905 trigger_hook = hooks_utils.trigger_update_pull_request_hook
905 trigger_hook = hooks_utils.trigger_update_pull_request_hook
906 elif action == 'comment':
906 elif action == 'comment':
907 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
907 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
908 elif action == 'comment_edit':
908 elif action == 'comment_edit':
909 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
909 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
910 else:
910 else:
911 return
911 return
912
912
913 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
913 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
914 pull_request, action, trigger_hook)
914 pull_request, action, trigger_hook)
915 trigger_hook(
915 trigger_hook(
916 username=user.username,
916 username=user.username,
917 repo_name=pull_request.target_repo.repo_name,
917 repo_name=pull_request.target_repo.repo_name,
918 repo_type=target_scm.alias,
918 repo_type=target_scm.alias,
919 pull_request=pull_request,
919 pull_request=pull_request,
920 data=data)
920 data=data)
921
921
922 def _get_commit_ids(self, pull_request):
922 def _get_commit_ids(self, pull_request):
923 """
923 """
924 Return the commit ids of the merged pull request.
924 Return the commit ids of the merged pull request.
925
925
926 This method is not dealing correctly yet with the lack of autoupdates
926 This method is not dealing correctly yet with the lack of autoupdates
927 nor with the implicit target updates.
927 nor with the implicit target updates.
928 For example: if a commit in the source repo is already in the target it
928 For example: if a commit in the source repo is already in the target it
929 will be reported anyways.
929 will be reported anyways.
930 """
930 """
931 merge_rev = pull_request.merge_rev
931 merge_rev = pull_request.merge_rev
932 if merge_rev is None:
932 if merge_rev is None:
933 raise ValueError('This pull request was not merged yet')
933 raise ValueError('This pull request was not merged yet')
934
934
935 commit_ids = list(pull_request.revisions)
935 commit_ids = list(pull_request.revisions)
936 if merge_rev not in commit_ids:
936 if merge_rev not in commit_ids:
937 commit_ids.append(merge_rev)
937 commit_ids.append(merge_rev)
938
938
939 return commit_ids
939 return commit_ids
940
940
941 def merge_repo(self, pull_request, user, extras):
941 def merge_repo(self, pull_request, user, extras):
942 repo_type = pull_request.source_repo.repo_type
942 repo_type = pull_request.source_repo.repo_type
943 log.debug("Merging pull request %s", pull_request)
943 log.debug("Merging pull request %s", pull_request)
944
944
945 extras['user_agent'] = '{}/internal-merge'.format(repo_type)
945 extras['user_agent'] = '{}/internal-merge'.format(repo_type)
946 merge_state = self._merge_pull_request(pull_request, user, extras)
946 merge_state = self._merge_pull_request(pull_request, user, extras)
947 if merge_state.executed:
947 if merge_state.executed:
948 log.debug("Merge was successful, updating the pull request comments.")
948 log.debug("Merge was successful, updating the pull request comments.")
949 self._comment_and_close_pr(pull_request, user, merge_state)
949 self._comment_and_close_pr(pull_request, user, merge_state)
950
950
951 self._log_audit_action(
951 self._log_audit_action(
952 'repo.pull_request.merge',
952 'repo.pull_request.merge',
953 {'merge_state': merge_state.__dict__},
953 {'merge_state': merge_state.__dict__},
954 user, pull_request)
954 user, pull_request)
955
955
956 else:
956 else:
957 log.warning("Merge failed, not updating the pull request.")
957 log.warning("Merge failed, not updating the pull request.")
958 return merge_state
958 return merge_state
959
959
960 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
960 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
961 target_vcs = pull_request.target_repo.scm_instance()
961 target_vcs = pull_request.target_repo.scm_instance()
962 source_vcs = pull_request.source_repo.scm_instance()
962 source_vcs = pull_request.source_repo.scm_instance()
963
963
964 message = safe_str(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
964 message = safe_str(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
965 pr_id=pull_request.pull_request_id,
965 pr_id=pull_request.pull_request_id,
966 pr_title=pull_request.title,
966 pr_title=pull_request.title,
967 pr_desc=pull_request.description,
967 pr_desc=pull_request.description,
968 source_repo=source_vcs.name,
968 source_repo=source_vcs.name,
969 source_ref_name=pull_request.source_ref_parts.name,
969 source_ref_name=pull_request.source_ref_parts.name,
970 target_repo=target_vcs.name,
970 target_repo=target_vcs.name,
971 target_ref_name=pull_request.target_ref_parts.name,
971 target_ref_name=pull_request.target_ref_parts.name,
972 )
972 )
973
973
974 workspace_id = self._workspace_id(pull_request)
974 workspace_id = self._workspace_id(pull_request)
975 repo_id = pull_request.target_repo.repo_id
975 repo_id = pull_request.target_repo.repo_id
976 use_rebase = self._use_rebase_for_merging(pull_request)
976 use_rebase = self._use_rebase_for_merging(pull_request)
977 close_branch = self._close_branch_before_merging(pull_request)
977 close_branch = self._close_branch_before_merging(pull_request)
978 user_name = self._user_name_for_merging(pull_request, user)
978 user_name = self._user_name_for_merging(pull_request, user)
979
979
980 target_ref = self._refresh_reference(
980 target_ref = self._refresh_reference(
981 pull_request.target_ref_parts, target_vcs)
981 pull_request.target_ref_parts, target_vcs)
982
982
983 callback_daemon, extras = prepare_callback_daemon(
983 callback_daemon, extras = prepare_callback_daemon(
984 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
984 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
985 host=vcs_settings.HOOKS_HOST)
985 host=vcs_settings.HOOKS_HOST)
986
986
987 with callback_daemon:
987 with callback_daemon:
988 # TODO: johbo: Implement a clean way to run a config_override
988 # TODO: johbo: Implement a clean way to run a config_override
989 # for a single call.
989 # for a single call.
990 target_vcs.config.set(
990 target_vcs.config.set(
991 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
991 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
992
992
993 merge_state = target_vcs.merge(
993 merge_state = target_vcs.merge(
994 repo_id, workspace_id, target_ref, source_vcs,
994 repo_id, workspace_id, target_ref, source_vcs,
995 pull_request.source_ref_parts,
995 pull_request.source_ref_parts,
996 user_name=user_name, user_email=user.email,
996 user_name=user_name, user_email=user.email,
997 message=message, use_rebase=use_rebase,
997 message=message, use_rebase=use_rebase,
998 close_branch=close_branch)
998 close_branch=close_branch)
999
999
1000 return merge_state
1000 return merge_state
1001
1001
1002 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
1002 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
1003 pull_request.merge_rev = merge_state.merge_ref.commit_id
1003 pull_request.merge_rev = merge_state.merge_ref.commit_id
1004 pull_request.updated_on = datetime.datetime.now()
1004 pull_request.updated_on = datetime.datetime.now()
1005 close_msg = close_msg or 'Pull request merged and closed'
1005 close_msg = close_msg or 'Pull request merged and closed'
1006
1006
1007 CommentsModel().create(
1007 CommentsModel().create(
1008 text=safe_str(close_msg),
1008 text=safe_str(close_msg),
1009 repo=pull_request.target_repo.repo_id,
1009 repo=pull_request.target_repo.repo_id,
1010 user=user.user_id,
1010 user=user.user_id,
1011 pull_request=pull_request.pull_request_id,
1011 pull_request=pull_request.pull_request_id,
1012 f_path=None,
1012 f_path=None,
1013 line_no=None,
1013 line_no=None,
1014 closing_pr=True
1014 closing_pr=True
1015 )
1015 )
1016
1016
1017 Session().add(pull_request)
1017 Session().add(pull_request)
1018 Session().flush()
1018 Session().flush()
1019 # TODO: paris: replace invalidation with less radical solution
1019 # TODO: paris: replace invalidation with less radical solution
1020 ScmModel().mark_for_invalidation(
1020 ScmModel().mark_for_invalidation(
1021 pull_request.target_repo.repo_name)
1021 pull_request.target_repo.repo_name)
1022 self.trigger_pull_request_hook(pull_request, user, 'merge')
1022 self.trigger_pull_request_hook(pull_request, user, 'merge')
1023
1023
1024 def has_valid_update_type(self, pull_request):
1024 def has_valid_update_type(self, pull_request):
1025 source_ref_type = pull_request.source_ref_parts.type
1025 source_ref_type = pull_request.source_ref_parts.type
1026 return source_ref_type in self.REF_TYPES
1026 return source_ref_type in self.REF_TYPES
1027
1027
1028 def get_flow_commits(self, pull_request):
1028 def get_flow_commits(self, pull_request):
1029
1029
1030 # source repo
1030 # source repo
1031 source_ref_name = pull_request.source_ref_parts.name
1031 source_ref_name = pull_request.source_ref_parts.name
1032 source_ref_type = pull_request.source_ref_parts.type
1032 source_ref_type = pull_request.source_ref_parts.type
1033 source_ref_id = pull_request.source_ref_parts.commit_id
1033 source_ref_id = pull_request.source_ref_parts.commit_id
1034 source_repo = pull_request.source_repo.scm_instance()
1034 source_repo = pull_request.source_repo.scm_instance()
1035
1035
1036 try:
1036 try:
1037 if source_ref_type in self.REF_TYPES:
1037 if source_ref_type in self.REF_TYPES:
1038 source_commit = source_repo.get_commit(
1038 source_commit = source_repo.get_commit(
1039 source_ref_name, reference_obj=pull_request.source_ref_parts)
1039 source_ref_name, reference_obj=pull_request.source_ref_parts)
1040 else:
1040 else:
1041 source_commit = source_repo.get_commit(source_ref_id)
1041 source_commit = source_repo.get_commit(source_ref_id)
1042 except CommitDoesNotExistError:
1042 except CommitDoesNotExistError:
1043 raise SourceRefMissing()
1043 raise SourceRefMissing()
1044
1044
1045 # target repo
1045 # target repo
1046 target_ref_name = pull_request.target_ref_parts.name
1046 target_ref_name = pull_request.target_ref_parts.name
1047 target_ref_type = pull_request.target_ref_parts.type
1047 target_ref_type = pull_request.target_ref_parts.type
1048 target_ref_id = pull_request.target_ref_parts.commit_id
1048 target_ref_id = pull_request.target_ref_parts.commit_id
1049 target_repo = pull_request.target_repo.scm_instance()
1049 target_repo = pull_request.target_repo.scm_instance()
1050
1050
1051 try:
1051 try:
1052 if target_ref_type in self.REF_TYPES:
1052 if target_ref_type in self.REF_TYPES:
1053 target_commit = target_repo.get_commit(
1053 target_commit = target_repo.get_commit(
1054 target_ref_name, reference_obj=pull_request.target_ref_parts)
1054 target_ref_name, reference_obj=pull_request.target_ref_parts)
1055 else:
1055 else:
1056 target_commit = target_repo.get_commit(target_ref_id)
1056 target_commit = target_repo.get_commit(target_ref_id)
1057 except CommitDoesNotExistError:
1057 except CommitDoesNotExistError:
1058 raise TargetRefMissing()
1058 raise TargetRefMissing()
1059
1059
1060 return source_commit, target_commit
1060 return source_commit, target_commit
1061
1061
1062 def update_commits(self, pull_request, updating_user):
1062 def update_commits(self, pull_request, updating_user):
1063 """
1063 """
1064 Get the updated list of commits for the pull request
1064 Get the updated list of commits for the pull request
1065 and return the new pull request version and the list
1065 and return the new pull request version and the list
1066 of commits processed by this update action
1066 of commits processed by this update action
1067
1067
1068 updating_user is the user_object who triggered the update
1068 updating_user is the user_object who triggered the update
1069 """
1069 """
1070 pull_request = self.__get_pull_request(pull_request)
1070 pull_request = self.__get_pull_request(pull_request)
1071 source_ref_type = pull_request.source_ref_parts.type
1071 source_ref_type = pull_request.source_ref_parts.type
1072 source_ref_name = pull_request.source_ref_parts.name
1072 source_ref_name = pull_request.source_ref_parts.name
1073 source_ref_id = pull_request.source_ref_parts.commit_id
1073 source_ref_id = pull_request.source_ref_parts.commit_id
1074
1074
1075 target_ref_type = pull_request.target_ref_parts.type
1075 target_ref_type = pull_request.target_ref_parts.type
1076 target_ref_name = pull_request.target_ref_parts.name
1076 target_ref_name = pull_request.target_ref_parts.name
1077 target_ref_id = pull_request.target_ref_parts.commit_id
1077 target_ref_id = pull_request.target_ref_parts.commit_id
1078
1078
1079 if not self.has_valid_update_type(pull_request):
1079 if not self.has_valid_update_type(pull_request):
1080 log.debug("Skipping update of pull request %s due to ref type: %s",
1080 log.debug("Skipping update of pull request %s due to ref type: %s",
1081 pull_request, source_ref_type)
1081 pull_request, source_ref_type)
1082 return UpdateResponse(
1082 return UpdateResponse(
1083 executed=False,
1083 executed=False,
1084 reason=UpdateFailureReason.WRONG_REF_TYPE,
1084 reason=UpdateFailureReason.WRONG_REF_TYPE,
1085 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1085 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1086 source_changed=False, target_changed=False)
1086 source_changed=False, target_changed=False)
1087
1087
1088 try:
1088 try:
1089 source_commit, target_commit = self.get_flow_commits(pull_request)
1089 source_commit, target_commit = self.get_flow_commits(pull_request)
1090 except SourceRefMissing:
1090 except SourceRefMissing:
1091 return UpdateResponse(
1091 return UpdateResponse(
1092 executed=False,
1092 executed=False,
1093 reason=UpdateFailureReason.MISSING_SOURCE_REF,
1093 reason=UpdateFailureReason.MISSING_SOURCE_REF,
1094 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1094 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1095 source_changed=False, target_changed=False)
1095 source_changed=False, target_changed=False)
1096 except TargetRefMissing:
1096 except TargetRefMissing:
1097 return UpdateResponse(
1097 return UpdateResponse(
1098 executed=False,
1098 executed=False,
1099 reason=UpdateFailureReason.MISSING_TARGET_REF,
1099 reason=UpdateFailureReason.MISSING_TARGET_REF,
1100 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1100 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1101 source_changed=False, target_changed=False)
1101 source_changed=False, target_changed=False)
1102
1102
1103 source_changed = source_ref_id != source_commit.raw_id
1103 source_changed = source_ref_id != source_commit.raw_id
1104 target_changed = target_ref_id != target_commit.raw_id
1104 target_changed = target_ref_id != target_commit.raw_id
1105
1105
1106 if not (source_changed or target_changed):
1106 if not (source_changed or target_changed):
1107 log.debug("Nothing changed in pull request %s", pull_request)
1107 log.debug("Nothing changed in pull request %s", pull_request)
1108 return UpdateResponse(
1108 return UpdateResponse(
1109 executed=False,
1109 executed=False,
1110 reason=UpdateFailureReason.NO_CHANGE,
1110 reason=UpdateFailureReason.NO_CHANGE,
1111 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1111 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1112 source_changed=target_changed, target_changed=source_changed)
1112 source_changed=target_changed, target_changed=source_changed)
1113
1113
1114 change_in_found = 'target repo' if target_changed else 'source repo'
1114 change_in_found = 'target repo' if target_changed else 'source repo'
1115 log.debug('Updating pull request because of change in %s detected',
1115 log.debug('Updating pull request because of change in %s detected',
1116 change_in_found)
1116 change_in_found)
1117
1117
1118 # Finally there is a need for an update, in case of source change
1118 # Finally there is a need for an update, in case of source change
1119 # we create a new version, else just an update
1119 # we create a new version, else just an update
1120 if source_changed:
1120 if source_changed:
1121 pull_request_version = self._create_version_from_snapshot(pull_request)
1121 pull_request_version = self._create_version_from_snapshot(pull_request)
1122 self._link_comments_to_version(pull_request_version)
1122 self._link_comments_to_version(pull_request_version)
1123 else:
1123 else:
1124 try:
1124 try:
1125 ver = pull_request.versions[-1]
1125 ver = pull_request.versions[-1]
1126 except IndexError:
1126 except IndexError:
1127 ver = None
1127 ver = None
1128
1128
1129 pull_request.pull_request_version_id = \
1129 pull_request.pull_request_version_id = \
1130 ver.pull_request_version_id if ver else None
1130 ver.pull_request_version_id if ver else None
1131 pull_request_version = pull_request
1131 pull_request_version = pull_request
1132
1132
1133 source_repo = pull_request.source_repo.scm_instance()
1133 source_repo = pull_request.source_repo.scm_instance()
1134 target_repo = pull_request.target_repo.scm_instance()
1134 target_repo = pull_request.target_repo.scm_instance()
1135
1135
1136 # re-compute commit ids
1136 # re-compute commit ids
1137 old_commit_ids = pull_request.revisions
1137 old_commit_ids = pull_request.revisions
1138 pre_load = ["author", "date", "message", "branch"]
1138 pre_load = ["author", "date", "message", "branch"]
1139 commit_ranges = target_repo.compare(
1139 commit_ranges = target_repo.compare(
1140 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
1140 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
1141 pre_load=pre_load)
1141 pre_load=pre_load)
1142
1142
1143 target_ref = target_commit.raw_id
1143 target_ref = target_commit.raw_id
1144 source_ref = source_commit.raw_id
1144 source_ref = source_commit.raw_id
1145 ancestor_commit_id = target_repo.get_common_ancestor(
1145 ancestor_commit_id = target_repo.get_common_ancestor(
1146 target_ref, source_ref, source_repo)
1146 target_ref, source_ref, source_repo)
1147
1147
1148 if not ancestor_commit_id:
1148 if not ancestor_commit_id:
1149 raise ValueError(
1149 raise ValueError(
1150 'cannot calculate diff info without a common ancestor. '
1150 'cannot calculate diff info without a common ancestor. '
1151 'Make sure both repositories are related, and have a common forking commit.')
1151 'Make sure both repositories are related, and have a common forking commit.')
1152
1152
1153 pull_request.common_ancestor_id = ancestor_commit_id
1153 pull_request.common_ancestor_id = ancestor_commit_id
1154
1154
1155 pull_request.source_ref = f'{source_ref_type}:{source_ref_name}:{source_commit.raw_id}'
1155 pull_request.source_ref = f'{source_ref_type}:{source_ref_name}:{source_commit.raw_id}'
1156 pull_request.target_ref = f'{target_ref_type}:{target_ref_name}:{ancestor_commit_id}'
1156 pull_request.target_ref = f'{target_ref_type}:{target_ref_name}:{ancestor_commit_id}'
1157
1157
1158 pull_request.revisions = [
1158 pull_request.revisions = [
1159 commit.raw_id for commit in reversed(commit_ranges)]
1159 commit.raw_id for commit in reversed(commit_ranges)]
1160 pull_request.updated_on = datetime.datetime.now()
1160 pull_request.updated_on = datetime.datetime.now()
1161 Session().add(pull_request)
1161 Session().add(pull_request)
1162 new_commit_ids = pull_request.revisions
1162 new_commit_ids = pull_request.revisions
1163
1163
1164 old_diff_data, new_diff_data = self._generate_update_diffs(
1164 old_diff_data, new_diff_data = self._generate_update_diffs(
1165 pull_request, pull_request_version)
1165 pull_request, pull_request_version)
1166
1166
1167 # calculate commit and file changes
1167 # calculate commit and file changes
1168 commit_changes = self._calculate_commit_id_changes(
1168 commit_changes = self._calculate_commit_id_changes(
1169 old_commit_ids, new_commit_ids)
1169 old_commit_ids, new_commit_ids)
1170 file_changes = self._calculate_file_changes(
1170 file_changes = self._calculate_file_changes(
1171 old_diff_data, new_diff_data)
1171 old_diff_data, new_diff_data)
1172
1172
1173 # set comments as outdated if DIFFS changed
1173 # set comments as outdated if DIFFS changed
1174 CommentsModel().outdate_comments(
1174 CommentsModel().outdate_comments(
1175 pull_request, old_diff_data=old_diff_data,
1175 pull_request, old_diff_data=old_diff_data,
1176 new_diff_data=new_diff_data)
1176 new_diff_data=new_diff_data)
1177
1177
1178 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1178 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1179 file_node_changes = (
1179 file_node_changes = (
1180 file_changes.added or file_changes.modified or file_changes.removed)
1180 file_changes.added or file_changes.modified or file_changes.removed)
1181 pr_has_changes = valid_commit_changes or file_node_changes
1181 pr_has_changes = valid_commit_changes or file_node_changes
1182
1182
1183 # Add an automatic comment to the pull request, in case
1183 # Add an automatic comment to the pull request, in case
1184 # anything has changed
1184 # anything has changed
1185 if pr_has_changes:
1185 if pr_has_changes:
1186 update_comment = CommentsModel().create(
1186 update_comment = CommentsModel().create(
1187 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1187 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1188 repo=pull_request.target_repo,
1188 repo=pull_request.target_repo,
1189 user=pull_request.author,
1189 user=pull_request.author,
1190 pull_request=pull_request,
1190 pull_request=pull_request,
1191 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1191 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1192
1192
1193 # Update status to "Under Review" for added commits
1193 # Update status to "Under Review" for added commits
1194 for commit_id in commit_changes.added:
1194 for commit_id in commit_changes.added:
1195 ChangesetStatusModel().set_status(
1195 ChangesetStatusModel().set_status(
1196 repo=pull_request.source_repo,
1196 repo=pull_request.source_repo,
1197 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1197 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1198 comment=update_comment,
1198 comment=update_comment,
1199 user=pull_request.author,
1199 user=pull_request.author,
1200 pull_request=pull_request,
1200 pull_request=pull_request,
1201 revision=commit_id)
1201 revision=commit_id)
1202
1202
1203 # initial commit
1203 # initial commit
1204 Session().commit()
1204 Session().commit()
1205
1205
1206 if pr_has_changes:
1206 if pr_has_changes:
1207 # send update email to users
1207 # send update email to users
1208 try:
1208 try:
1209 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1209 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1210 ancestor_commit_id=ancestor_commit_id,
1210 ancestor_commit_id=ancestor_commit_id,
1211 commit_changes=commit_changes,
1211 commit_changes=commit_changes,
1212 file_changes=file_changes)
1212 file_changes=file_changes)
1213 Session().commit()
1213 Session().commit()
1214 except Exception:
1214 except Exception:
1215 log.exception('Failed to send email notification to users')
1215 log.exception('Failed to send email notification to users')
1216 Session().rollback()
1216 Session().rollback()
1217
1217
1218 log.debug(
1218 log.debug(
1219 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1219 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1220 'removed_ids: %s', pull_request.pull_request_id,
1220 'removed_ids: %s', pull_request.pull_request_id,
1221 commit_changes.added, commit_changes.common, commit_changes.removed)
1221 commit_changes.added, commit_changes.common, commit_changes.removed)
1222 log.debug(
1222 log.debug(
1223 'Updated pull request with the following file changes: %s',
1223 'Updated pull request with the following file changes: %s',
1224 file_changes)
1224 file_changes)
1225
1225
1226 log.info(
1226 log.info(
1227 "Updated pull request %s from commit %s to commit %s, "
1227 "Updated pull request %s from commit %s to commit %s, "
1228 "stored new version %s of this pull request.",
1228 "stored new version %s of this pull request.",
1229 pull_request.pull_request_id, source_ref_id,
1229 pull_request.pull_request_id, source_ref_id,
1230 pull_request.source_ref_parts.commit_id,
1230 pull_request.source_ref_parts.commit_id,
1231 pull_request_version.pull_request_version_id)
1231 pull_request_version.pull_request_version_id)
1232
1232
1233 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1233 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1234
1234
1235 return UpdateResponse(
1235 return UpdateResponse(
1236 executed=True, reason=UpdateFailureReason.NONE,
1236 executed=True, reason=UpdateFailureReason.NONE,
1237 old=pull_request, new=pull_request_version,
1237 old=pull_request, new=pull_request_version,
1238 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1238 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1239 source_changed=source_changed, target_changed=target_changed)
1239 source_changed=source_changed, target_changed=target_changed)
1240
1240
1241 def _create_version_from_snapshot(self, pull_request):
1241 def _create_version_from_snapshot(self, pull_request):
1242 version = PullRequestVersion()
1242 version = PullRequestVersion()
1243 version.title = pull_request.title
1243 version.title = pull_request.title
1244 version.description = pull_request.description
1244 version.description = pull_request.description
1245 version.status = pull_request.status
1245 version.status = pull_request.status
1246 version.pull_request_state = pull_request.pull_request_state
1246 version.pull_request_state = pull_request.pull_request_state
1247 version.created_on = datetime.datetime.now()
1247 version.created_on = datetime.datetime.now()
1248 version.updated_on = pull_request.updated_on
1248 version.updated_on = pull_request.updated_on
1249 version.user_id = pull_request.user_id
1249 version.user_id = pull_request.user_id
1250 version.source_repo = pull_request.source_repo
1250 version.source_repo = pull_request.source_repo
1251 version.source_ref = pull_request.source_ref
1251 version.source_ref = pull_request.source_ref
1252 version.target_repo = pull_request.target_repo
1252 version.target_repo = pull_request.target_repo
1253 version.target_ref = pull_request.target_ref
1253 version.target_ref = pull_request.target_ref
1254
1254
1255 version._last_merge_source_rev = pull_request._last_merge_source_rev
1255 version._last_merge_source_rev = pull_request._last_merge_source_rev
1256 version._last_merge_target_rev = pull_request._last_merge_target_rev
1256 version._last_merge_target_rev = pull_request._last_merge_target_rev
1257 version.last_merge_status = pull_request.last_merge_status
1257 version.last_merge_status = pull_request.last_merge_status
1258 version.last_merge_metadata = pull_request.last_merge_metadata
1258 version.last_merge_metadata = pull_request.last_merge_metadata
1259 version.shadow_merge_ref = pull_request.shadow_merge_ref
1259 version.shadow_merge_ref = pull_request.shadow_merge_ref
1260 version.merge_rev = pull_request.merge_rev
1260 version.merge_rev = pull_request.merge_rev
1261 version.reviewer_data = pull_request.reviewer_data
1261 version.reviewer_data = pull_request.reviewer_data
1262
1262
1263 version.revisions = pull_request.revisions
1263 version.revisions = pull_request.revisions
1264 version.common_ancestor_id = pull_request.common_ancestor_id
1264 version.common_ancestor_id = pull_request.common_ancestor_id
1265 version.pull_request = pull_request
1265 version.pull_request = pull_request
1266 Session().add(version)
1266 Session().add(version)
1267 Session().flush()
1267 Session().flush()
1268
1268
1269 return version
1269 return version
1270
1270
1271 def _generate_update_diffs(self, pull_request, pull_request_version):
1271 def _generate_update_diffs(self, pull_request, pull_request_version):
1272
1272
1273 diff_context = (
1273 diff_context = (
1274 self.DIFF_CONTEXT +
1274 self.DIFF_CONTEXT +
1275 CommentsModel.needed_extra_diff_context())
1275 CommentsModel.needed_extra_diff_context())
1276 hide_whitespace_changes = False
1276 hide_whitespace_changes = False
1277 source_repo = pull_request_version.source_repo
1277 source_repo = pull_request_version.source_repo
1278 source_ref_id = pull_request_version.source_ref_parts.commit_id
1278 source_ref_id = pull_request_version.source_ref_parts.commit_id
1279 target_ref_id = pull_request_version.target_ref_parts.commit_id
1279 target_ref_id = pull_request_version.target_ref_parts.commit_id
1280 old_diff = self._get_diff_from_pr_or_version(
1280 old_diff = self._get_diff_from_pr_or_version(
1281 source_repo, source_ref_id, target_ref_id,
1281 source_repo, source_ref_id, target_ref_id,
1282 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1282 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1283
1283
1284 source_repo = pull_request.source_repo
1284 source_repo = pull_request.source_repo
1285 source_ref_id = pull_request.source_ref_parts.commit_id
1285 source_ref_id = pull_request.source_ref_parts.commit_id
1286 target_ref_id = pull_request.target_ref_parts.commit_id
1286 target_ref_id = pull_request.target_ref_parts.commit_id
1287
1287
1288 new_diff = self._get_diff_from_pr_or_version(
1288 new_diff = self._get_diff_from_pr_or_version(
1289 source_repo, source_ref_id, target_ref_id,
1289 source_repo, source_ref_id, target_ref_id,
1290 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1290 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1291
1291
1292 # NOTE: this was using diff_format='gitdiff'
1292 # NOTE: this was using diff_format='gitdiff'
1293 old_diff_data = diffs.DiffProcessor(old_diff, diff_format='newdiff')
1293 old_diff_data = diffs.DiffProcessor(old_diff, diff_format='newdiff')
1294 old_diff_data.prepare()
1294 old_diff_data.prepare()
1295 new_diff_data = diffs.DiffProcessor(new_diff, diff_format='newdiff')
1295 new_diff_data = diffs.DiffProcessor(new_diff, diff_format='newdiff')
1296 new_diff_data.prepare()
1296 new_diff_data.prepare()
1297
1297
1298 return old_diff_data, new_diff_data
1298 return old_diff_data, new_diff_data
1299
1299
1300 def _link_comments_to_version(self, pull_request_version):
1300 def _link_comments_to_version(self, pull_request_version):
1301 """
1301 """
1302 Link all unlinked comments of this pull request to the given version.
1302 Link all unlinked comments of this pull request to the given version.
1303
1303
1304 :param pull_request_version: The `PullRequestVersion` to which
1304 :param pull_request_version: The `PullRequestVersion` to which
1305 the comments shall be linked.
1305 the comments shall be linked.
1306
1306
1307 """
1307 """
1308 pull_request = pull_request_version.pull_request
1308 pull_request = pull_request_version.pull_request
1309 comments = ChangesetComment.query()\
1309 comments = ChangesetComment.query()\
1310 .filter(
1310 .filter(
1311 # TODO: johbo: Should we query for the repo at all here?
1311 # TODO: johbo: Should we query for the repo at all here?
1312 # Pending decision on how comments of PRs are to be related
1312 # Pending decision on how comments of PRs are to be related
1313 # to either the source repo, the target repo or no repo at all.
1313 # to either the source repo, the target repo or no repo at all.
1314 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1314 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1315 ChangesetComment.pull_request == pull_request,
1315 ChangesetComment.pull_request == pull_request,
1316 ChangesetComment.pull_request_version == null())\
1316 ChangesetComment.pull_request_version == null())\
1317 .order_by(ChangesetComment.comment_id.asc())
1317 .order_by(ChangesetComment.comment_id.asc())
1318
1318
1319 # TODO: johbo: Find out why this breaks if it is done in a bulk
1319 # TODO: johbo: Find out why this breaks if it is done in a bulk
1320 # operation.
1320 # operation.
1321 for comment in comments:
1321 for comment in comments:
1322 comment.pull_request_version_id = (
1322 comment.pull_request_version_id = (
1323 pull_request_version.pull_request_version_id)
1323 pull_request_version.pull_request_version_id)
1324 Session().add(comment)
1324 Session().add(comment)
1325
1325
1326 def _calculate_commit_id_changes(self, old_ids, new_ids):
1326 def _calculate_commit_id_changes(self, old_ids, new_ids):
1327 added = [x for x in new_ids if x not in old_ids]
1327 added = [x for x in new_ids if x not in old_ids]
1328 common = [x for x in new_ids if x in old_ids]
1328 common = [x for x in new_ids if x in old_ids]
1329 removed = [x for x in old_ids if x not in new_ids]
1329 removed = [x for x in old_ids if x not in new_ids]
1330 total = new_ids
1330 total = new_ids
1331 return ChangeTuple(added, common, removed, total)
1331 return ChangeTuple(added, common, removed, total)
1332
1332
1333 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1333 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1334
1334
1335 old_files = OrderedDict()
1335 old_files = OrderedDict()
1336 for diff_data in old_diff_data.parsed_diff:
1336 for diff_data in old_diff_data.parsed_diff:
1337 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1337 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1338
1338
1339 added_files = []
1339 added_files = []
1340 modified_files = []
1340 modified_files = []
1341 removed_files = []
1341 removed_files = []
1342 for diff_data in new_diff_data.parsed_diff:
1342 for diff_data in new_diff_data.parsed_diff:
1343 new_filename = diff_data['filename']
1343 new_filename = diff_data['filename']
1344 new_hash = md5_safe(diff_data['raw_diff'])
1344 new_hash = md5_safe(diff_data['raw_diff'])
1345
1345
1346 old_hash = old_files.get(new_filename)
1346 old_hash = old_files.get(new_filename)
1347 if not old_hash:
1347 if not old_hash:
1348 # file is not present in old diff, we have to figure out from parsed diff
1348 # file is not present in old diff, we have to figure out from parsed diff
1349 # operation ADD/REMOVE
1349 # operation ADD/REMOVE
1350 operations_dict = diff_data['stats']['ops']
1350 operations_dict = diff_data['stats']['ops']
1351 if diffs.DEL_FILENODE in operations_dict:
1351 if diffs.DEL_FILENODE in operations_dict:
1352 removed_files.append(new_filename)
1352 removed_files.append(new_filename)
1353 else:
1353 else:
1354 added_files.append(new_filename)
1354 added_files.append(new_filename)
1355 else:
1355 else:
1356 if new_hash != old_hash:
1356 if new_hash != old_hash:
1357 modified_files.append(new_filename)
1357 modified_files.append(new_filename)
1358 # now remove a file from old, since we have seen it already
1358 # now remove a file from old, since we have seen it already
1359 del old_files[new_filename]
1359 del old_files[new_filename]
1360
1360
1361 # removed files is when there are present in old, but not in NEW,
1361 # removed files is when there are present in old, but not in NEW,
1362 # since we remove old files that are present in new diff, left-overs
1362 # since we remove old files that are present in new diff, left-overs
1363 # if any should be the removed files
1363 # if any should be the removed files
1364 removed_files.extend(old_files.keys())
1364 removed_files.extend(old_files.keys())
1365
1365
1366 return FileChangeTuple(added_files, modified_files, removed_files)
1366 return FileChangeTuple(added_files, modified_files, removed_files)
1367
1367
1368 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1368 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1369 """
1369 """
1370 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1370 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1371 so it's always looking the same disregarding on which default
1371 so it's always looking the same disregarding on which default
1372 renderer system is using.
1372 renderer system is using.
1373
1373
1374 :param ancestor_commit_id: ancestor raw_id
1374 :param ancestor_commit_id: ancestor raw_id
1375 :param changes: changes named tuple
1375 :param changes: changes named tuple
1376 :param file_changes: file changes named tuple
1376 :param file_changes: file changes named tuple
1377
1377
1378 """
1378 """
1379 new_status = ChangesetStatus.get_status_lbl(
1379 new_status = ChangesetStatus.get_status_lbl(
1380 ChangesetStatus.STATUS_UNDER_REVIEW)
1380 ChangesetStatus.STATUS_UNDER_REVIEW)
1381
1381
1382 changed_files = (
1382 changed_files = (
1383 file_changes.added + file_changes.modified + file_changes.removed)
1383 file_changes.added + file_changes.modified + file_changes.removed)
1384
1384
1385 params = {
1385 params = {
1386 'under_review_label': new_status,
1386 'under_review_label': new_status,
1387 'added_commits': changes.added,
1387 'added_commits': changes.added,
1388 'removed_commits': changes.removed,
1388 'removed_commits': changes.removed,
1389 'changed_files': changed_files,
1389 'changed_files': changed_files,
1390 'added_files': file_changes.added,
1390 'added_files': file_changes.added,
1391 'modified_files': file_changes.modified,
1391 'modified_files': file_changes.modified,
1392 'removed_files': file_changes.removed,
1392 'removed_files': file_changes.removed,
1393 'ancestor_commit_id': ancestor_commit_id
1393 'ancestor_commit_id': ancestor_commit_id
1394 }
1394 }
1395 renderer = RstTemplateRenderer()
1395 renderer = RstTemplateRenderer()
1396 return renderer.render('pull_request_update.mako', **params)
1396 return renderer.render('pull_request_update.mako', **params)
1397
1397
1398 def edit(self, pull_request, title, description, description_renderer, user):
1398 def edit(self, pull_request, title, description, description_renderer, user):
1399 pull_request = self.__get_pull_request(pull_request)
1399 pull_request = self.__get_pull_request(pull_request)
1400 old_data = pull_request.get_api_data(with_merge_state=False)
1400 old_data = pull_request.get_api_data(with_merge_state=False)
1401 if pull_request.is_closed():
1401 if pull_request.is_closed():
1402 raise ValueError('This pull request is closed')
1402 raise ValueError('This pull request is closed')
1403 if title:
1403 if title:
1404 pull_request.title = title
1404 pull_request.title = title
1405 pull_request.description = description
1405 pull_request.description = description
1406 pull_request.updated_on = datetime.datetime.now()
1406 pull_request.updated_on = datetime.datetime.now()
1407 pull_request.description_renderer = description_renderer
1407 pull_request.description_renderer = description_renderer
1408 Session().add(pull_request)
1408 Session().add(pull_request)
1409 self._log_audit_action(
1409 self._log_audit_action(
1410 'repo.pull_request.edit', {'old_data': old_data},
1410 'repo.pull_request.edit', {'old_data': old_data},
1411 user, pull_request)
1411 user, pull_request)
1412
1412
1413 def update_reviewers(self, pull_request, reviewer_data, user):
1413 def update_reviewers(self, pull_request, reviewer_data, user):
1414 """
1414 """
1415 Update the reviewers in the pull request
1415 Update the reviewers in the pull request
1416
1416
1417 :param pull_request: the pr to update
1417 :param pull_request: the pr to update
1418 :param reviewer_data: list of tuples
1418 :param reviewer_data: list of tuples
1419 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1419 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1420 :param user: current use who triggers this action
1420 :param user: current use who triggers this action
1421 """
1421 """
1422
1422
1423 pull_request = self.__get_pull_request(pull_request)
1423 pull_request = self.__get_pull_request(pull_request)
1424 if pull_request.is_closed():
1424 if pull_request.is_closed():
1425 raise ValueError('This pull request is closed')
1425 raise ValueError('This pull request is closed')
1426
1426
1427 reviewers = {}
1427 reviewers = {}
1428 for user_id, reasons, mandatory, role, rules in reviewer_data:
1428 for user_id, reasons, mandatory, role, rules in reviewer_data:
1429 if isinstance(user_id, (int, str)):
1429 if isinstance(user_id, (int, str)):
1430 user_id = self._get_user(user_id).user_id
1430 user_id = self._get_user(user_id).user_id
1431 reviewers[user_id] = {
1431 reviewers[user_id] = {
1432 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1432 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1433
1433
1434 reviewers_ids = set(reviewers.keys())
1434 reviewers_ids = set(reviewers.keys())
1435 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1435 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1436 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1436 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1437
1437
1438 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1438 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1439
1439
1440 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1440 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1441 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1441 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1442
1442
1443 log.debug("Adding %s reviewers", ids_to_add)
1443 log.debug("Adding %s reviewers", ids_to_add)
1444 log.debug("Removing %s reviewers", ids_to_remove)
1444 log.debug("Removing %s reviewers", ids_to_remove)
1445 changed = False
1445 changed = False
1446 added_audit_reviewers = []
1446 added_audit_reviewers = []
1447 removed_audit_reviewers = []
1447 removed_audit_reviewers = []
1448
1448
1449 for uid in ids_to_add:
1449 for uid in ids_to_add:
1450 changed = True
1450 changed = True
1451 _usr = self._get_user(uid)
1451 _usr = self._get_user(uid)
1452 reviewer = PullRequestReviewers()
1452 reviewer = PullRequestReviewers()
1453 reviewer.user = _usr
1453 reviewer.user = _usr
1454 reviewer.pull_request = pull_request
1454 reviewer.pull_request = pull_request
1455 reviewer.reasons = reviewers[uid]['reasons']
1455 reviewer.reasons = reviewers[uid]['reasons']
1456 # NOTE(marcink): mandatory shouldn't be changed now
1456 # NOTE(marcink): mandatory shouldn't be changed now
1457 # reviewer.mandatory = reviewers[uid]['reasons']
1457 # reviewer.mandatory = reviewers[uid]['reasons']
1458 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1458 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1459 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1459 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1460 Session().add(reviewer)
1460 Session().add(reviewer)
1461 added_audit_reviewers.append(reviewer.get_dict())
1461 added_audit_reviewers.append(reviewer.get_dict())
1462
1462
1463 for uid in ids_to_remove:
1463 for uid in ids_to_remove:
1464 changed = True
1464 changed = True
1465 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1465 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1466 # This is an edge case that handles previous state of having the same reviewer twice.
1466 # This is an edge case that handles previous state of having the same reviewer twice.
1467 # this CAN happen due to the lack of DB checks
1467 # this CAN happen due to the lack of DB checks
1468 reviewers = PullRequestReviewers.query()\
1468 reviewers = PullRequestReviewers.query()\
1469 .filter(PullRequestReviewers.user_id == uid,
1469 .filter(PullRequestReviewers.user_id == uid,
1470 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1470 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1471 PullRequestReviewers.pull_request == pull_request)\
1471 PullRequestReviewers.pull_request == pull_request)\
1472 .all()
1472 .all()
1473
1473
1474 for obj in reviewers:
1474 for obj in reviewers:
1475 added_audit_reviewers.append(obj.get_dict())
1475 added_audit_reviewers.append(obj.get_dict())
1476 Session().delete(obj)
1476 Session().delete(obj)
1477
1477
1478 if changed:
1478 if changed:
1479 Session().expire_all()
1479 Session().expire_all()
1480 pull_request.updated_on = datetime.datetime.now()
1480 pull_request.updated_on = datetime.datetime.now()
1481 Session().add(pull_request)
1481 Session().add(pull_request)
1482
1482
1483 # finally store audit logs
1483 # finally store audit logs
1484 for user_data in added_audit_reviewers:
1484 for user_data in added_audit_reviewers:
1485 self._log_audit_action(
1485 self._log_audit_action(
1486 'repo.pull_request.reviewer.add', {'data': user_data},
1486 'repo.pull_request.reviewer.add', {'data': user_data},
1487 user, pull_request)
1487 user, pull_request)
1488 for user_data in removed_audit_reviewers:
1488 for user_data in removed_audit_reviewers:
1489 self._log_audit_action(
1489 self._log_audit_action(
1490 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1490 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1491 user, pull_request)
1491 user, pull_request)
1492
1492
1493 self.notify_reviewers(pull_request, ids_to_add, user)
1493 self.notify_reviewers(pull_request, ids_to_add, user)
1494 return ids_to_add, ids_to_remove
1494 return ids_to_add, ids_to_remove
1495
1495
1496 def update_observers(self, pull_request, observer_data, user):
1496 def update_observers(self, pull_request, observer_data, user):
1497 """
1497 """
1498 Update the observers in the pull request
1498 Update the observers in the pull request
1499
1499
1500 :param pull_request: the pr to update
1500 :param pull_request: the pr to update
1501 :param observer_data: list of tuples
1501 :param observer_data: list of tuples
1502 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1502 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1503 :param user: current use who triggers this action
1503 :param user: current use who triggers this action
1504 """
1504 """
1505 pull_request = self.__get_pull_request(pull_request)
1505 pull_request = self.__get_pull_request(pull_request)
1506 if pull_request.is_closed():
1506 if pull_request.is_closed():
1507 raise ValueError('This pull request is closed')
1507 raise ValueError('This pull request is closed')
1508
1508
1509 observers = {}
1509 observers = {}
1510 for user_id, reasons, mandatory, role, rules in observer_data:
1510 for user_id, reasons, mandatory, role, rules in observer_data:
1511 if isinstance(user_id, (int, str)):
1511 if isinstance(user_id, (int, str)):
1512 user_id = self._get_user(user_id).user_id
1512 user_id = self._get_user(user_id).user_id
1513 observers[user_id] = {
1513 observers[user_id] = {
1514 'reasons': reasons, 'observers': mandatory, 'role': role}
1514 'reasons': reasons, 'observers': mandatory, 'role': role}
1515
1515
1516 observers_ids = set(observers.keys())
1516 observers_ids = set(observers.keys())
1517 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1517 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1518 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1518 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1519
1519
1520 current_observers_ids = set([x.user.user_id for x in current_observers])
1520 current_observers_ids = set([x.user.user_id for x in current_observers])
1521
1521
1522 ids_to_add = observers_ids.difference(current_observers_ids)
1522 ids_to_add = observers_ids.difference(current_observers_ids)
1523 ids_to_remove = current_observers_ids.difference(observers_ids)
1523 ids_to_remove = current_observers_ids.difference(observers_ids)
1524
1524
1525 log.debug("Adding %s observer", ids_to_add)
1525 log.debug("Adding %s observer", ids_to_add)
1526 log.debug("Removing %s observer", ids_to_remove)
1526 log.debug("Removing %s observer", ids_to_remove)
1527 changed = False
1527 changed = False
1528 added_audit_observers = []
1528 added_audit_observers = []
1529 removed_audit_observers = []
1529 removed_audit_observers = []
1530
1530
1531 for uid in ids_to_add:
1531 for uid in ids_to_add:
1532 changed = True
1532 changed = True
1533 _usr = self._get_user(uid)
1533 _usr = self._get_user(uid)
1534 observer = PullRequestReviewers()
1534 observer = PullRequestReviewers()
1535 observer.user = _usr
1535 observer.user = _usr
1536 observer.pull_request = pull_request
1536 observer.pull_request = pull_request
1537 observer.reasons = observers[uid]['reasons']
1537 observer.reasons = observers[uid]['reasons']
1538 # NOTE(marcink): mandatory shouldn't be changed now
1538 # NOTE(marcink): mandatory shouldn't be changed now
1539 # observer.mandatory = observer[uid]['reasons']
1539 # observer.mandatory = observer[uid]['reasons']
1540
1540
1541 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1541 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1542 observer.role = PullRequestReviewers.ROLE_OBSERVER
1542 observer.role = PullRequestReviewers.ROLE_OBSERVER
1543 Session().add(observer)
1543 Session().add(observer)
1544 added_audit_observers.append(observer.get_dict())
1544 added_audit_observers.append(observer.get_dict())
1545
1545
1546 for uid in ids_to_remove:
1546 for uid in ids_to_remove:
1547 changed = True
1547 changed = True
1548 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1548 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1549 # This is an edge case that handles previous state of having the same reviewer twice.
1549 # This is an edge case that handles previous state of having the same reviewer twice.
1550 # this CAN happen due to the lack of DB checks
1550 # this CAN happen due to the lack of DB checks
1551 observers = PullRequestReviewers.query()\
1551 observers = PullRequestReviewers.query()\
1552 .filter(PullRequestReviewers.user_id == uid,
1552 .filter(PullRequestReviewers.user_id == uid,
1553 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1553 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1554 PullRequestReviewers.pull_request == pull_request)\
1554 PullRequestReviewers.pull_request == pull_request)\
1555 .all()
1555 .all()
1556
1556
1557 for obj in observers:
1557 for obj in observers:
1558 added_audit_observers.append(obj.get_dict())
1558 added_audit_observers.append(obj.get_dict())
1559 Session().delete(obj)
1559 Session().delete(obj)
1560
1560
1561 if changed:
1561 if changed:
1562 Session().expire_all()
1562 Session().expire_all()
1563 pull_request.updated_on = datetime.datetime.now()
1563 pull_request.updated_on = datetime.datetime.now()
1564 Session().add(pull_request)
1564 Session().add(pull_request)
1565
1565
1566 # finally store audit logs
1566 # finally store audit logs
1567 for user_data in added_audit_observers:
1567 for user_data in added_audit_observers:
1568 self._log_audit_action(
1568 self._log_audit_action(
1569 'repo.pull_request.observer.add', {'data': user_data},
1569 'repo.pull_request.observer.add', {'data': user_data},
1570 user, pull_request)
1570 user, pull_request)
1571 for user_data in removed_audit_observers:
1571 for user_data in removed_audit_observers:
1572 self._log_audit_action(
1572 self._log_audit_action(
1573 'repo.pull_request.observer.delete', {'old_data': user_data},
1573 'repo.pull_request.observer.delete', {'old_data': user_data},
1574 user, pull_request)
1574 user, pull_request)
1575
1575
1576 self.notify_observers(pull_request, ids_to_add, user)
1576 self.notify_observers(pull_request, ids_to_add, user)
1577 return ids_to_add, ids_to_remove
1577 return ids_to_add, ids_to_remove
1578
1578
1579 def get_url(self, pull_request, request=None, permalink=False):
1579 def get_url(self, pull_request, request=None, permalink=False):
1580 if not request:
1580 if not request:
1581 request = get_current_request()
1581 request = get_current_request()
1582
1582
1583 if permalink:
1583 if permalink:
1584 return request.route_url(
1584 return request.route_url(
1585 'pull_requests_global',
1585 'pull_requests_global',
1586 pull_request_id=pull_request.pull_request_id,)
1586 pull_request_id=pull_request.pull_request_id,)
1587 else:
1587 else:
1588 return request.route_url('pullrequest_show',
1588 return request.route_url('pullrequest_show',
1589 repo_name=safe_str(pull_request.target_repo.repo_name),
1589 repo_name=safe_str(pull_request.target_repo.repo_name),
1590 pull_request_id=pull_request.pull_request_id,)
1590 pull_request_id=pull_request.pull_request_id,)
1591
1591
1592 def get_shadow_clone_url(self, pull_request, request=None):
1592 def get_shadow_clone_url(self, pull_request, request=None):
1593 """
1593 """
1594 Returns qualified url pointing to the shadow repository. If this pull
1594 Returns qualified url pointing to the shadow repository. If this pull
1595 request is closed there is no shadow repository and ``None`` will be
1595 request is closed there is no shadow repository and ``None`` will be
1596 returned.
1596 returned.
1597 """
1597 """
1598 if pull_request.is_closed():
1598 if pull_request.is_closed():
1599 return None
1599 return None
1600 else:
1600 else:
1601 pr_url = urllib.parse.unquote(self.get_url(pull_request, request=request))
1601 pr_url = urllib.parse.unquote(self.get_url(pull_request, request=request))
1602 return safe_str('{pr_url}/repository'.format(pr_url=pr_url))
1602 return safe_str('{pr_url}/repository'.format(pr_url=pr_url))
1603
1603
1604 def _notify_reviewers(self, pull_request, user_ids, role, user):
1604 def _notify_reviewers(self, pull_request, user_ids, role, user):
1605 # notification to reviewers/observers
1605 # notification to reviewers/observers
1606 if not user_ids:
1606 if not user_ids:
1607 return
1607 return
1608
1608
1609 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1609 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1610
1610
1611 pull_request_obj = pull_request
1611 pull_request_obj = pull_request
1612 # get the current participants of this pull request
1612 # get the current participants of this pull request
1613 recipients = user_ids
1613 recipients = user_ids
1614 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1614 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1615
1615
1616 pr_source_repo = pull_request_obj.source_repo
1616 pr_source_repo = pull_request_obj.source_repo
1617 pr_target_repo = pull_request_obj.target_repo
1617 pr_target_repo = pull_request_obj.target_repo
1618
1618
1619 pr_url = h.route_url('pullrequest_show',
1619 pr_url = h.route_url('pullrequest_show',
1620 repo_name=pr_target_repo.repo_name,
1620 repo_name=pr_target_repo.repo_name,
1621 pull_request_id=pull_request_obj.pull_request_id,)
1621 pull_request_id=pull_request_obj.pull_request_id,)
1622
1622
1623 # set some variables for email notification
1623 # set some variables for email notification
1624 pr_target_repo_url = h.route_url(
1624 pr_target_repo_url = h.route_url(
1625 'repo_summary', repo_name=pr_target_repo.repo_name)
1625 'repo_summary', repo_name=pr_target_repo.repo_name)
1626
1626
1627 pr_source_repo_url = h.route_url(
1627 pr_source_repo_url = h.route_url(
1628 'repo_summary', repo_name=pr_source_repo.repo_name)
1628 'repo_summary', repo_name=pr_source_repo.repo_name)
1629
1629
1630 # pull request specifics
1630 # pull request specifics
1631 pull_request_commits = [
1631 pull_request_commits = [
1632 (x.raw_id, x.message)
1632 (x.raw_id, x.message)
1633 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1633 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1634
1634
1635 current_rhodecode_user = user
1635 current_rhodecode_user = user
1636 kwargs = {
1636 kwargs = {
1637 'user': current_rhodecode_user,
1637 'user': current_rhodecode_user,
1638 'pull_request_author': pull_request.author,
1638 'pull_request_author': pull_request.author,
1639 'pull_request': pull_request_obj,
1639 'pull_request': pull_request_obj,
1640 'pull_request_commits': pull_request_commits,
1640 'pull_request_commits': pull_request_commits,
1641
1641
1642 'pull_request_target_repo': pr_target_repo,
1642 'pull_request_target_repo': pr_target_repo,
1643 'pull_request_target_repo_url': pr_target_repo_url,
1643 'pull_request_target_repo_url': pr_target_repo_url,
1644
1644
1645 'pull_request_source_repo': pr_source_repo,
1645 'pull_request_source_repo': pr_source_repo,
1646 'pull_request_source_repo_url': pr_source_repo_url,
1646 'pull_request_source_repo_url': pr_source_repo_url,
1647
1647
1648 'pull_request_url': pr_url,
1648 'pull_request_url': pr_url,
1649 'thread_ids': [pr_url],
1649 'thread_ids': [pr_url],
1650 'user_role': role
1650 'user_role': role
1651 }
1651 }
1652
1652
1653 # create notification objects, and emails
1653 # create notification objects, and emails
1654 NotificationModel().create(
1654 NotificationModel().create(
1655 created_by=current_rhodecode_user,
1655 created_by=current_rhodecode_user,
1656 notification_subject='', # Filled in based on the notification_type
1656 notification_subject='', # Filled in based on the notification_type
1657 notification_body='', # Filled in based on the notification_type
1657 notification_body='', # Filled in based on the notification_type
1658 notification_type=notification_type,
1658 notification_type=notification_type,
1659 recipients=recipients,
1659 recipients=recipients,
1660 email_kwargs=kwargs,
1660 email_kwargs=kwargs,
1661 )
1661 )
1662
1662
1663 def notify_reviewers(self, pull_request, reviewers_ids, user):
1663 def notify_reviewers(self, pull_request, reviewers_ids, user):
1664 return self._notify_reviewers(pull_request, reviewers_ids,
1664 return self._notify_reviewers(pull_request, reviewers_ids,
1665 PullRequestReviewers.ROLE_REVIEWER, user)
1665 PullRequestReviewers.ROLE_REVIEWER, user)
1666
1666
1667 def notify_observers(self, pull_request, observers_ids, user):
1667 def notify_observers(self, pull_request, observers_ids, user):
1668 return self._notify_reviewers(pull_request, observers_ids,
1668 return self._notify_reviewers(pull_request, observers_ids,
1669 PullRequestReviewers.ROLE_OBSERVER, user)
1669 PullRequestReviewers.ROLE_OBSERVER, user)
1670
1670
1671 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1671 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1672 commit_changes, file_changes):
1672 commit_changes, file_changes):
1673
1673
1674 updating_user_id = updating_user.user_id
1674 updating_user_id = updating_user.user_id
1675 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1675 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1676 # NOTE(marcink): send notification to all other users except to
1676 # NOTE(marcink): send notification to all other users except to
1677 # person who updated the PR
1677 # person who updated the PR
1678 recipients = reviewers.difference(set([updating_user_id]))
1678 recipients = reviewers.difference(set([updating_user_id]))
1679
1679
1680 log.debug('Notify following recipients about pull-request update %s', recipients)
1680 log.debug('Notify following recipients about pull-request update %s', recipients)
1681
1681
1682 pull_request_obj = pull_request
1682 pull_request_obj = pull_request
1683
1683
1684 # send email about the update
1684 # send email about the update
1685 changed_files = (
1685 changed_files = (
1686 file_changes.added + file_changes.modified + file_changes.removed)
1686 file_changes.added + file_changes.modified + file_changes.removed)
1687
1687
1688 pr_source_repo = pull_request_obj.source_repo
1688 pr_source_repo = pull_request_obj.source_repo
1689 pr_target_repo = pull_request_obj.target_repo
1689 pr_target_repo = pull_request_obj.target_repo
1690
1690
1691 pr_url = h.route_url('pullrequest_show',
1691 pr_url = h.route_url('pullrequest_show',
1692 repo_name=pr_target_repo.repo_name,
1692 repo_name=pr_target_repo.repo_name,
1693 pull_request_id=pull_request_obj.pull_request_id,)
1693 pull_request_id=pull_request_obj.pull_request_id,)
1694
1694
1695 # set some variables for email notification
1695 # set some variables for email notification
1696 pr_target_repo_url = h.route_url(
1696 pr_target_repo_url = h.route_url(
1697 'repo_summary', repo_name=pr_target_repo.repo_name)
1697 'repo_summary', repo_name=pr_target_repo.repo_name)
1698
1698
1699 pr_source_repo_url = h.route_url(
1699 pr_source_repo_url = h.route_url(
1700 'repo_summary', repo_name=pr_source_repo.repo_name)
1700 'repo_summary', repo_name=pr_source_repo.repo_name)
1701
1701
1702 email_kwargs = {
1702 email_kwargs = {
1703 'date': datetime.datetime.now(),
1703 'date': datetime.datetime.now(),
1704 'updating_user': updating_user,
1704 'updating_user': updating_user,
1705
1705
1706 'pull_request': pull_request_obj,
1706 'pull_request': pull_request_obj,
1707
1707
1708 'pull_request_target_repo': pr_target_repo,
1708 'pull_request_target_repo': pr_target_repo,
1709 'pull_request_target_repo_url': pr_target_repo_url,
1709 'pull_request_target_repo_url': pr_target_repo_url,
1710
1710
1711 'pull_request_source_repo': pr_source_repo,
1711 'pull_request_source_repo': pr_source_repo,
1712 'pull_request_source_repo_url': pr_source_repo_url,
1712 'pull_request_source_repo_url': pr_source_repo_url,
1713
1713
1714 'pull_request_url': pr_url,
1714 'pull_request_url': pr_url,
1715
1715
1716 'ancestor_commit_id': ancestor_commit_id,
1716 'ancestor_commit_id': ancestor_commit_id,
1717 'added_commits': commit_changes.added,
1717 'added_commits': commit_changes.added,
1718 'removed_commits': commit_changes.removed,
1718 'removed_commits': commit_changes.removed,
1719 'changed_files': changed_files,
1719 'changed_files': changed_files,
1720 'added_files': file_changes.added,
1720 'added_files': file_changes.added,
1721 'modified_files': file_changes.modified,
1721 'modified_files': file_changes.modified,
1722 'removed_files': file_changes.removed,
1722 'removed_files': file_changes.removed,
1723 'thread_ids': [pr_url],
1723 'thread_ids': [pr_url],
1724 }
1724 }
1725
1725
1726 # create notification objects, and emails
1726 # create notification objects, and emails
1727 NotificationModel().create(
1727 NotificationModel().create(
1728 created_by=updating_user,
1728 created_by=updating_user,
1729 notification_subject='', # Filled in based on the notification_type
1729 notification_subject='', # Filled in based on the notification_type
1730 notification_body='', # Filled in based on the notification_type
1730 notification_body='', # Filled in based on the notification_type
1731 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1731 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1732 recipients=recipients,
1732 recipients=recipients,
1733 email_kwargs=email_kwargs,
1733 email_kwargs=email_kwargs,
1734 )
1734 )
1735
1735
1736 def delete(self, pull_request, user=None):
1736 def delete(self, pull_request, user=None):
1737 if not user:
1737 if not user:
1738 user = getattr(get_current_rhodecode_user(), 'username', None)
1738 user = getattr(get_current_rhodecode_user(), 'username', None)
1739
1739
1740 pull_request = self.__get_pull_request(pull_request)
1740 pull_request = self.__get_pull_request(pull_request)
1741 old_data = pull_request.get_api_data(with_merge_state=False)
1741 old_data = pull_request.get_api_data(with_merge_state=False)
1742 self._cleanup_merge_workspace(pull_request)
1742 self._cleanup_merge_workspace(pull_request)
1743 self._log_audit_action(
1743 self._log_audit_action(
1744 'repo.pull_request.delete', {'old_data': old_data},
1744 'repo.pull_request.delete', {'old_data': old_data},
1745 user, pull_request)
1745 user, pull_request)
1746 Session().delete(pull_request)
1746 Session().delete(pull_request)
1747
1747
1748 def close_pull_request(self, pull_request, user):
1748 def close_pull_request(self, pull_request, user):
1749 pull_request = self.__get_pull_request(pull_request)
1749 pull_request = self.__get_pull_request(pull_request)
1750 self._cleanup_merge_workspace(pull_request)
1750 self._cleanup_merge_workspace(pull_request)
1751 pull_request.status = PullRequest.STATUS_CLOSED
1751 pull_request.status = PullRequest.STATUS_CLOSED
1752 pull_request.updated_on = datetime.datetime.now()
1752 pull_request.updated_on = datetime.datetime.now()
1753 Session().add(pull_request)
1753 Session().add(pull_request)
1754 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1754 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1755
1755
1756 pr_data = pull_request.get_api_data(with_merge_state=False)
1756 pr_data = pull_request.get_api_data(with_merge_state=False)
1757 self._log_audit_action(
1757 self._log_audit_action(
1758 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1758 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1759
1759
1760 def close_pull_request_with_comment(
1760 def close_pull_request_with_comment(
1761 self, pull_request, user, repo, message=None, auth_user=None):
1761 self, pull_request, user, repo, message=None, auth_user=None):
1762
1762
1763 pull_request_review_status = pull_request.calculated_review_status()
1763 pull_request_review_status = pull_request.calculated_review_status()
1764
1764
1765 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1765 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1766 # approved only if we have voting consent
1766 # approved only if we have voting consent
1767 status = ChangesetStatus.STATUS_APPROVED
1767 status = ChangesetStatus.STATUS_APPROVED
1768 else:
1768 else:
1769 status = ChangesetStatus.STATUS_REJECTED
1769 status = ChangesetStatus.STATUS_REJECTED
1770 status_lbl = ChangesetStatus.get_status_lbl(status)
1770 status_lbl = ChangesetStatus.get_status_lbl(status)
1771
1771
1772 default_message = (
1772 default_message = (
1773 'Closing with status change {transition_icon} {status}.'
1773 'Closing with status change {transition_icon} {status}.'
1774 ).format(transition_icon='>', status=status_lbl)
1774 ).format(transition_icon='>', status=status_lbl)
1775 text = message or default_message
1775 text = message or default_message
1776
1776
1777 # create a comment, and link it to new status
1777 # create a comment, and link it to new status
1778 comment = CommentsModel().create(
1778 comment = CommentsModel().create(
1779 text=text,
1779 text=text,
1780 repo=repo.repo_id,
1780 repo=repo.repo_id,
1781 user=user.user_id,
1781 user=user.user_id,
1782 pull_request=pull_request.pull_request_id,
1782 pull_request=pull_request.pull_request_id,
1783 status_change=status_lbl,
1783 status_change=status_lbl,
1784 status_change_type=status,
1784 status_change_type=status,
1785 closing_pr=True,
1785 closing_pr=True,
1786 auth_user=auth_user,
1786 auth_user=auth_user,
1787 )
1787 )
1788
1788
1789 # calculate old status before we change it
1789 # calculate old status before we change it
1790 old_calculated_status = pull_request.calculated_review_status()
1790 old_calculated_status = pull_request.calculated_review_status()
1791 ChangesetStatusModel().set_status(
1791 ChangesetStatusModel().set_status(
1792 repo.repo_id,
1792 repo.repo_id,
1793 status,
1793 status,
1794 user.user_id,
1794 user.user_id,
1795 comment=comment,
1795 comment=comment,
1796 pull_request=pull_request.pull_request_id
1796 pull_request=pull_request.pull_request_id
1797 )
1797 )
1798
1798
1799 Session().flush()
1799 Session().flush()
1800
1800
1801 self.trigger_pull_request_hook(pull_request, user, 'comment',
1801 self.trigger_pull_request_hook(pull_request, user, 'comment',
1802 data={'comment': comment})
1802 data={'comment': comment})
1803
1803
1804 # we now calculate the status of pull request again, and based on that
1804 # we now calculate the status of pull request again, and based on that
1805 # calculation trigger status change. This might happen in cases
1805 # calculation trigger status change. This might happen in cases
1806 # that non-reviewer admin closes a pr, which means his vote doesn't
1806 # that non-reviewer admin closes a pr, which means his vote doesn't
1807 # change the status, while if he's a reviewer this might change it.
1807 # change the status, while if he's a reviewer this might change it.
1808 calculated_status = pull_request.calculated_review_status()
1808 calculated_status = pull_request.calculated_review_status()
1809 if old_calculated_status != calculated_status:
1809 if old_calculated_status != calculated_status:
1810 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1810 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1811 data={'status': calculated_status})
1811 data={'status': calculated_status})
1812
1812
1813 # finally close the PR
1813 # finally close the PR
1814 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1814 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1815
1815
1816 return comment, status
1816 return comment, status
1817
1817
1818 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1818 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1819 _ = translator or get_current_request().translate
1819 _ = translator or get_current_request().translate
1820
1820
1821 if not self._is_merge_enabled(pull_request):
1821 if not self._is_merge_enabled(pull_request):
1822 return None, False, _('Server-side pull request merging is disabled.')
1822 return None, False, _('Server-side pull request merging is disabled.')
1823
1823
1824 if pull_request.is_closed():
1824 if pull_request.is_closed():
1825 return None, False, _('This pull request is closed.')
1825 return None, False, _('This pull request is closed.')
1826
1826
1827 merge_possible, msg = self._check_repo_requirements(
1827 merge_possible, msg = self._check_repo_requirements(
1828 target=pull_request.target_repo, source=pull_request.source_repo,
1828 target=pull_request.target_repo, source=pull_request.source_repo,
1829 translator=_)
1829 translator=_)
1830 if not merge_possible:
1830 if not merge_possible:
1831 return None, merge_possible, msg
1831 return None, merge_possible, msg
1832
1832
1833 try:
1833 try:
1834 merge_response = self._try_merge(
1834 merge_response = self._try_merge(
1835 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1835 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1836 log.debug("Merge response: %s", merge_response)
1836 log.debug("Merge response: %s", merge_response)
1837 return merge_response, merge_response.possible, merge_response.merge_status_message
1837 return merge_response, merge_response.possible, merge_response.merge_status_message
1838 except NotImplementedError:
1838 except NotImplementedError:
1839 return None, False, _('Pull request merging is not supported.')
1839 return None, False, _('Pull request merging is not supported.')
1840
1840
1841 def _check_repo_requirements(self, target, source, translator):
1841 def _check_repo_requirements(self, target, source, translator):
1842 """
1842 """
1843 Check if `target` and `source` have compatible requirements.
1843 Check if `target` and `source` have compatible requirements.
1844
1844
1845 Currently this is just checking for largefiles.
1845 Currently this is just checking for largefiles.
1846 """
1846 """
1847 _ = translator
1847 _ = translator
1848 target_has_largefiles = self._has_largefiles(target)
1848 target_has_largefiles = self._has_largefiles(target)
1849 source_has_largefiles = self._has_largefiles(source)
1849 source_has_largefiles = self._has_largefiles(source)
1850 merge_possible = True
1850 merge_possible = True
1851 message = u''
1851 message = u''
1852
1852
1853 if target_has_largefiles != source_has_largefiles:
1853 if target_has_largefiles != source_has_largefiles:
1854 merge_possible = False
1854 merge_possible = False
1855 if source_has_largefiles:
1855 if source_has_largefiles:
1856 message = _(
1856 message = _(
1857 'Target repository large files support is disabled.')
1857 'Target repository large files support is disabled.')
1858 else:
1858 else:
1859 message = _(
1859 message = _(
1860 'Source repository large files support is disabled.')
1860 'Source repository large files support is disabled.')
1861
1861
1862 return merge_possible, message
1862 return merge_possible, message
1863
1863
1864 def _has_largefiles(self, repo):
1864 def _has_largefiles(self, repo):
1865 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1865 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1866 'extensions', 'largefiles')
1866 'extensions', 'largefiles')
1867 return largefiles_ui and largefiles_ui[0].active
1867 return largefiles_ui and largefiles_ui[0].active
1868
1868
1869 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1869 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1870 """
1870 """
1871 Try to merge the pull request and return the merge status.
1871 Try to merge the pull request and return the merge status.
1872 """
1872 """
1873 log.debug(
1873 log.debug(
1874 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1874 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1875 pull_request.pull_request_id, force_shadow_repo_refresh)
1875 pull_request.pull_request_id, force_shadow_repo_refresh)
1876 target_vcs = pull_request.target_repo.scm_instance()
1876 target_vcs = pull_request.target_repo.scm_instance()
1877 # Refresh the target reference.
1877 # Refresh the target reference.
1878 try:
1878 try:
1879 target_ref = self._refresh_reference(
1879 target_ref = self._refresh_reference(
1880 pull_request.target_ref_parts, target_vcs)
1880 pull_request.target_ref_parts, target_vcs)
1881 except CommitDoesNotExistError:
1881 except CommitDoesNotExistError:
1882 merge_state = MergeResponse(
1882 merge_state = MergeResponse(
1883 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1883 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1884 metadata={'target_ref': pull_request.target_ref_parts})
1884 metadata={'target_ref': pull_request.target_ref_parts})
1885 return merge_state
1885 return merge_state
1886
1886
1887 target_locked = pull_request.target_repo.locked
1887 target_locked = pull_request.target_repo.locked
1888 if target_locked and target_locked[0]:
1888 if target_locked and target_locked[0]:
1889 locked_by = 'user:{}'.format(target_locked[0])
1889 locked_by = 'user:{}'.format(target_locked[0])
1890 log.debug("The target repository is locked by %s.", locked_by)
1890 log.debug("The target repository is locked by %s.", locked_by)
1891 merge_state = MergeResponse(
1891 merge_state = MergeResponse(
1892 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1892 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1893 metadata={'locked_by': locked_by})
1893 metadata={'locked_by': locked_by})
1894 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1894 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1895 pull_request, target_ref):
1895 pull_request, target_ref):
1896 log.debug("Refreshing the merge status of the repository.")
1896 log.debug("Refreshing the merge status of the repository.")
1897 merge_state = self._refresh_merge_state(
1897 merge_state = self._refresh_merge_state(
1898 pull_request, target_vcs, target_ref)
1898 pull_request, target_vcs, target_ref)
1899 else:
1899 else:
1900 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1900 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1901 metadata = {
1901 metadata = {
1902 'unresolved_files': '',
1902 'unresolved_files': '',
1903 'target_ref': pull_request.target_ref_parts,
1903 'target_ref': pull_request.target_ref_parts,
1904 'source_ref': pull_request.source_ref_parts,
1904 'source_ref': pull_request.source_ref_parts,
1905 }
1905 }
1906 if pull_request.last_merge_metadata:
1906 if pull_request.last_merge_metadata:
1907 metadata.update(pull_request.last_merge_metadata_parsed)
1907 metadata.update(pull_request.last_merge_metadata_parsed)
1908
1908
1909 if not possible and target_ref.type == 'branch':
1909 if not possible and target_ref.type == 'branch':
1910 # NOTE(marcink): case for mercurial multiple heads on branch
1910 # NOTE(marcink): case for mercurial multiple heads on branch
1911 heads = target_vcs._heads(target_ref.name)
1911 heads = target_vcs._heads(target_ref.name)
1912 if len(heads) != 1:
1912 if len(heads) != 1:
1913 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1913 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1914 metadata.update({
1914 metadata.update({
1915 'heads': heads
1915 'heads': heads
1916 })
1916 })
1917
1917
1918 merge_state = MergeResponse(
1918 merge_state = MergeResponse(
1919 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1919 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1920
1920
1921 return merge_state
1921 return merge_state
1922
1922
1923 def _refresh_reference(self, reference, vcs_repository):
1923 def _refresh_reference(self, reference, vcs_repository):
1924 if reference.type in self.UPDATABLE_REF_TYPES:
1924 if reference.type in self.UPDATABLE_REF_TYPES:
1925 name_or_id = reference.name
1925 name_or_id = reference.name
1926 else:
1926 else:
1927 name_or_id = reference.commit_id
1927 name_or_id = reference.commit_id
1928
1928
1929 refreshed_commit = vcs_repository.get_commit(name_or_id)
1929 refreshed_commit = vcs_repository.get_commit(name_or_id)
1930 refreshed_reference = Reference(
1930 refreshed_reference = Reference(
1931 reference.type, reference.name, refreshed_commit.raw_id)
1931 reference.type, reference.name, refreshed_commit.raw_id)
1932 return refreshed_reference
1932 return refreshed_reference
1933
1933
1934 def _needs_merge_state_refresh(self, pull_request, target_reference):
1934 def _needs_merge_state_refresh(self, pull_request, target_reference):
1935 return not(
1935 return not(
1936 pull_request.revisions and
1936 pull_request.revisions and
1937 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1937 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1938 target_reference.commit_id == pull_request._last_merge_target_rev)
1938 target_reference.commit_id == pull_request._last_merge_target_rev)
1939
1939
1940 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1940 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1941 workspace_id = self._workspace_id(pull_request)
1941 workspace_id = self._workspace_id(pull_request)
1942 source_vcs = pull_request.source_repo.scm_instance()
1942 source_vcs = pull_request.source_repo.scm_instance()
1943 repo_id = pull_request.target_repo.repo_id
1943 repo_id = pull_request.target_repo.repo_id
1944 use_rebase = self._use_rebase_for_merging(pull_request)
1944 use_rebase = self._use_rebase_for_merging(pull_request)
1945 close_branch = self._close_branch_before_merging(pull_request)
1945 close_branch = self._close_branch_before_merging(pull_request)
1946 merge_state = target_vcs.merge(
1946 merge_state = target_vcs.merge(
1947 repo_id, workspace_id,
1947 repo_id, workspace_id,
1948 target_reference, source_vcs, pull_request.source_ref_parts,
1948 target_reference, source_vcs, pull_request.source_ref_parts,
1949 dry_run=True, use_rebase=use_rebase,
1949 dry_run=True, use_rebase=use_rebase,
1950 close_branch=close_branch)
1950 close_branch=close_branch)
1951
1951
1952 # Do not store the response if there was an unknown error.
1952 # Do not store the response if there was an unknown error.
1953 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1953 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1954 pull_request._last_merge_source_rev = \
1954 pull_request._last_merge_source_rev = \
1955 pull_request.source_ref_parts.commit_id
1955 pull_request.source_ref_parts.commit_id
1956 pull_request._last_merge_target_rev = target_reference.commit_id
1956 pull_request._last_merge_target_rev = target_reference.commit_id
1957 pull_request.last_merge_status = merge_state.failure_reason
1957 pull_request.last_merge_status = merge_state.failure_reason
1958 pull_request.last_merge_metadata = merge_state.metadata
1958 pull_request.last_merge_metadata = merge_state.metadata
1959
1959
1960 pull_request.shadow_merge_ref = merge_state.merge_ref
1960 pull_request.shadow_merge_ref = merge_state.merge_ref
1961 Session().add(pull_request)
1961 Session().add(pull_request)
1962 Session().commit()
1962 Session().commit()
1963
1963
1964 return merge_state
1964 return merge_state
1965
1965
1966 def _workspace_id(self, pull_request):
1966 def _workspace_id(self, pull_request):
1967 workspace_id = 'pr-%s' % pull_request.pull_request_id
1967 workspace_id = 'pr-%s' % pull_request.pull_request_id
1968 return workspace_id
1968 return workspace_id
1969
1969
1970 def generate_repo_data(self, repo, commit_id=None, branch=None,
1970 def generate_repo_data(self, repo, commit_id=None, branch=None,
1971 bookmark=None, translator=None):
1971 bookmark=None, translator=None):
1972 from rhodecode.model.repo import RepoModel
1972 from rhodecode.model.repo import RepoModel
1973
1973
1974 all_refs, selected_ref = \
1974 all_refs, selected_ref = \
1975 self._get_repo_pullrequest_sources(
1975 self._get_repo_pullrequest_sources(
1976 repo.scm_instance(), commit_id=commit_id,
1976 repo.scm_instance(), commit_id=commit_id,
1977 branch=branch, bookmark=bookmark, translator=translator)
1977 branch=branch, bookmark=bookmark, translator=translator)
1978
1978
1979 refs_select2 = []
1979 refs_select2 = []
1980 for element in all_refs:
1980 for element in all_refs:
1981 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1981 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1982 refs_select2.append({'text': element[1], 'children': children})
1982 refs_select2.append({'text': element[1], 'children': children})
1983
1983
1984 return {
1984 return {
1985 'user': {
1985 'user': {
1986 'user_id': repo.user.user_id,
1986 'user_id': repo.user.user_id,
1987 'username': repo.user.username,
1987 'username': repo.user.username,
1988 'firstname': repo.user.first_name,
1988 'firstname': repo.user.first_name,
1989 'lastname': repo.user.last_name,
1989 'lastname': repo.user.last_name,
1990 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1990 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1991 },
1991 },
1992 'name': repo.repo_name,
1992 'name': repo.repo_name,
1993 'link': RepoModel().get_url(repo),
1993 'link': RepoModel().get_url(repo),
1994 'description': h.chop_at_smart(repo.description_safe, '\n'),
1994 'description': h.chop_at_smart(repo.description_safe, '\n'),
1995 'refs': {
1995 'refs': {
1996 'all_refs': all_refs,
1996 'all_refs': all_refs,
1997 'selected_ref': selected_ref,
1997 'selected_ref': selected_ref,
1998 'select2_refs': refs_select2
1998 'select2_refs': refs_select2
1999 }
1999 }
2000 }
2000 }
2001
2001
2002 def generate_pullrequest_title(self, source, source_ref, target):
2002 def generate_pullrequest_title(self, source, source_ref, target):
2003 return u'{source}#{at_ref} to {target}'.format(
2003 return u'{source}#{at_ref} to {target}'.format(
2004 source=source,
2004 source=source,
2005 at_ref=source_ref,
2005 at_ref=source_ref,
2006 target=target,
2006 target=target,
2007 )
2007 )
2008
2008
2009 def _cleanup_merge_workspace(self, pull_request):
2009 def _cleanup_merge_workspace(self, pull_request):
2010 # Merging related cleanup
2010 # Merging related cleanup
2011 repo_id = pull_request.target_repo.repo_id
2011 repo_id = pull_request.target_repo.repo_id
2012 target_scm = pull_request.target_repo.scm_instance()
2012 target_scm = pull_request.target_repo.scm_instance()
2013 workspace_id = self._workspace_id(pull_request)
2013 workspace_id = self._workspace_id(pull_request)
2014
2014
2015 try:
2015 try:
2016 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
2016 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
2017 except NotImplementedError:
2017 except NotImplementedError:
2018 pass
2018 pass
2019
2019
2020 def _get_repo_pullrequest_sources(
2020 def _get_repo_pullrequest_sources(
2021 self, repo, commit_id=None, branch=None, bookmark=None,
2021 self, repo, commit_id=None, branch=None, bookmark=None,
2022 translator=None):
2022 translator=None):
2023 """
2023 """
2024 Return a structure with repo's interesting commits, suitable for
2024 Return a structure with repo's interesting commits, suitable for
2025 the selectors in pullrequest controller
2025 the selectors in pullrequest controller
2026
2026
2027 :param commit_id: a commit that must be in the list somehow
2027 :param commit_id: a commit that must be in the list somehow
2028 and selected by default
2028 and selected by default
2029 :param branch: a branch that must be in the list and selected
2029 :param branch: a branch that must be in the list and selected
2030 by default - even if closed
2030 by default - even if closed
2031 :param bookmark: a bookmark that must be in the list and selected
2031 :param bookmark: a bookmark that must be in the list and selected
2032 """
2032 """
2033 _ = translator or get_current_request().translate
2033 _ = translator or get_current_request().translate
2034
2034
2035 commit_id = safe_str(commit_id) if commit_id else None
2035 commit_id = safe_str(commit_id) if commit_id else None
2036 branch = safe_str(branch) if branch else None
2036 branch = safe_str(branch) if branch else None
2037 bookmark = safe_str(bookmark) if bookmark else None
2037 bookmark = safe_str(bookmark) if bookmark else None
2038
2038
2039 selected = None
2039 selected = None
2040
2040
2041 # order matters: first source that has commit_id in it will be selected
2041 # order matters: first source that has commit_id in it will be selected
2042 sources = []
2042 sources = []
2043 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
2043 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
2044 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
2044 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
2045
2045
2046 if commit_id:
2046 if commit_id:
2047 ref_commit = (h.short_id(commit_id), commit_id)
2047 ref_commit = (h.short_id(commit_id), commit_id)
2048 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
2048 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
2049
2049
2050 sources.append(
2050 sources.append(
2051 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
2051 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
2052 )
2052 )
2053
2053
2054 groups = []
2054 groups = []
2055
2055
2056 for group_key, ref_list, group_name, match in sources:
2056 for group_key, ref_list, group_name, match in sources:
2057 group_refs = []
2057 group_refs = []
2058 for ref_name, ref_id in ref_list:
2058 for ref_name, ref_id in ref_list:
2059 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
2059 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
2060 group_refs.append((ref_key, ref_name))
2060 group_refs.append((ref_key, ref_name))
2061
2061
2062 if not selected:
2062 if not selected:
2063 if set([commit_id, match]) & set([ref_id, ref_name]):
2063 if set([commit_id, match]) & set([ref_id, ref_name]):
2064 selected = ref_key
2064 selected = ref_key
2065
2065
2066 if group_refs:
2066 if group_refs:
2067 groups.append((group_refs, group_name))
2067 groups.append((group_refs, group_name))
2068
2068
2069 if not selected:
2069 if not selected:
2070 ref = commit_id or branch or bookmark
2070 ref = commit_id or branch or bookmark
2071 if ref:
2071 if ref:
2072 raise CommitDoesNotExistError(
2072 raise CommitDoesNotExistError(
2073 u'No commit refs could be found matching: {}'.format(ref))
2073 u'No commit refs could be found matching: {}'.format(ref))
2074 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
2074 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
2075 selected = u'branch:{}:{}'.format(
2075 selected = u'branch:{}:{}'.format(
2076 safe_str(repo.DEFAULT_BRANCH_NAME),
2076 safe_str(repo.DEFAULT_BRANCH_NAME),
2077 safe_str(repo.branches[repo.DEFAULT_BRANCH_NAME])
2077 safe_str(repo.branches[repo.DEFAULT_BRANCH_NAME])
2078 )
2078 )
2079 elif repo.commit_ids:
2079 elif repo.commit_ids:
2080 # make the user select in this case
2080 # make the user select in this case
2081 selected = None
2081 selected = None
2082 else:
2082 else:
2083 raise EmptyRepositoryError()
2083 raise EmptyRepositoryError()
2084 return groups, selected
2084 return groups, selected
2085
2085
2086 def get_diff(self, source_repo, source_ref_id, target_ref_id,
2086 def get_diff(self, source_repo, source_ref_id, target_ref_id,
2087 hide_whitespace_changes, diff_context):
2087 hide_whitespace_changes, diff_context):
2088
2088
2089 return self._get_diff_from_pr_or_version(
2089 return self._get_diff_from_pr_or_version(
2090 source_repo, source_ref_id, target_ref_id,
2090 source_repo, source_ref_id, target_ref_id,
2091 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
2091 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
2092
2092
2093 def _get_diff_from_pr_or_version(
2093 def _get_diff_from_pr_or_version(
2094 self, source_repo, source_ref_id, target_ref_id,
2094 self, source_repo, source_ref_id, target_ref_id,
2095 hide_whitespace_changes, diff_context):
2095 hide_whitespace_changes, diff_context):
2096
2096
2097 target_commit = source_repo.get_commit(
2097 target_commit = source_repo.get_commit(
2098 commit_id=safe_str(target_ref_id))
2098 commit_id=safe_str(target_ref_id))
2099 source_commit = source_repo.get_commit(
2099 source_commit = source_repo.get_commit(
2100 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
2100 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
2101 if isinstance(source_repo, Repository):
2101 if isinstance(source_repo, Repository):
2102 vcs_repo = source_repo.scm_instance()
2102 vcs_repo = source_repo.scm_instance()
2103 else:
2103 else:
2104 vcs_repo = source_repo
2104 vcs_repo = source_repo
2105
2105
2106 # TODO: johbo: In the context of an update, we cannot reach
2106 # TODO: johbo: In the context of an update, we cannot reach
2107 # the old commit anymore with our normal mechanisms. It needs
2107 # the old commit anymore with our normal mechanisms. It needs
2108 # some sort of special support in the vcs layer to avoid this
2108 # some sort of special support in the vcs layer to avoid this
2109 # workaround.
2109 # workaround.
2110 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
2110 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
2111 vcs_repo.alias == 'git'):
2111 vcs_repo.alias == 'git'):
2112 source_commit.raw_id = safe_str(source_ref_id)
2112 source_commit.raw_id = safe_str(source_ref_id)
2113
2113
2114 log.debug('calculating diff between '
2114 log.debug('calculating diff between '
2115 'source_ref:%s and target_ref:%s for repo `%s`',
2115 'source_ref:%s and target_ref:%s for repo `%s`',
2116 target_ref_id, source_ref_id,
2116 target_ref_id, source_ref_id,
2117 safe_str(vcs_repo.path))
2117 safe_str(vcs_repo.path))
2118
2118
2119 vcs_diff = vcs_repo.get_diff(
2119 vcs_diff = vcs_repo.get_diff(
2120 commit1=target_commit, commit2=source_commit,
2120 commit1=target_commit, commit2=source_commit,
2121 ignore_whitespace=hide_whitespace_changes, context=diff_context)
2121 ignore_whitespace=hide_whitespace_changes, context=diff_context)
2122 return vcs_diff
2122 return vcs_diff
2123
2123
2124 def _is_merge_enabled(self, pull_request):
2124 def _is_merge_enabled(self, pull_request):
2125 return self._get_general_setting(
2125 return self._get_general_setting(
2126 pull_request, 'rhodecode_pr_merge_enabled')
2126 pull_request, 'rhodecode_pr_merge_enabled')
2127
2127
2128 def is_automatic_merge_enabled(self, pull_request):
2129 return self._get_general_setting(
2130 pull_request, 'rhodecode_auto_merge_enabled')
2131
2128 def _use_rebase_for_merging(self, pull_request):
2132 def _use_rebase_for_merging(self, pull_request):
2129 repo_type = pull_request.target_repo.repo_type
2133 repo_type = pull_request.target_repo.repo_type
2130 if repo_type == 'hg':
2134 if repo_type == 'hg':
2131 return self._get_general_setting(
2135 return self._get_general_setting(
2132 pull_request, 'rhodecode_hg_use_rebase_for_merging')
2136 pull_request, 'rhodecode_hg_use_rebase_for_merging')
2133 elif repo_type == 'git':
2137 elif repo_type == 'git':
2134 return self._get_general_setting(
2138 return self._get_general_setting(
2135 pull_request, 'rhodecode_git_use_rebase_for_merging')
2139 pull_request, 'rhodecode_git_use_rebase_for_merging')
2136
2140
2137 return False
2141 return False
2138
2142
2139 def _user_name_for_merging(self, pull_request, user):
2143 def _user_name_for_merging(self, pull_request, user):
2140 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
2144 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
2141 if env_user_name_attr and hasattr(user, env_user_name_attr):
2145 if env_user_name_attr and hasattr(user, env_user_name_attr):
2142 user_name_attr = env_user_name_attr
2146 user_name_attr = env_user_name_attr
2143 else:
2147 else:
2144 user_name_attr = 'short_contact'
2148 user_name_attr = 'short_contact'
2145
2149
2146 user_name = getattr(user, user_name_attr)
2150 user_name = getattr(user, user_name_attr)
2147 return user_name
2151 return user_name
2148
2152
2149 def _close_branch_before_merging(self, pull_request):
2153 def _close_branch_before_merging(self, pull_request):
2150 repo_type = pull_request.target_repo.repo_type
2154 repo_type = pull_request.target_repo.repo_type
2151 if repo_type == 'hg':
2155 if repo_type == 'hg':
2152 return self._get_general_setting(
2156 return self._get_general_setting(
2153 pull_request, 'rhodecode_hg_close_branch_before_merging')
2157 pull_request, 'rhodecode_hg_close_branch_before_merging')
2154 elif repo_type == 'git':
2158 elif repo_type == 'git':
2155 return self._get_general_setting(
2159 return self._get_general_setting(
2156 pull_request, 'rhodecode_git_close_branch_before_merging')
2160 pull_request, 'rhodecode_git_close_branch_before_merging')
2157
2161
2158 return False
2162 return False
2159
2163
2160 def _get_general_setting(self, pull_request, settings_key, default=False):
2164 def _get_general_setting(self, pull_request, settings_key, default=False):
2161 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2165 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2162 settings = settings_model.get_general_settings()
2166 settings = settings_model.get_general_settings()
2163 return settings.get(settings_key, default)
2167 return settings.get(settings_key, default)
2164
2168
2165 def _log_audit_action(self, action, action_data, user, pull_request):
2169 def _log_audit_action(self, action, action_data, user, pull_request):
2166 audit_logger.store(
2170 audit_logger.store(
2167 action=action,
2171 action=action,
2168 action_data=action_data,
2172 action_data=action_data,
2169 user=user,
2173 user=user,
2170 repo=pull_request.target_repo)
2174 repo=pull_request.target_repo)
2171
2175
2172 def get_reviewer_functions(self):
2176 def get_reviewer_functions(self):
2173 """
2177 """
2174 Fetches functions for validation and fetching default reviewers.
2178 Fetches functions for validation and fetching default reviewers.
2175 If available we use the EE package, else we fallback to CE
2179 If available we use the EE package, else we fallback to CE
2176 package functions
2180 package functions
2177 """
2181 """
2178 try:
2182 try:
2179 from rc_reviewers.utils import get_default_reviewers_data
2183 from rc_reviewers.utils import get_default_reviewers_data
2180 from rc_reviewers.utils import validate_default_reviewers
2184 from rc_reviewers.utils import validate_default_reviewers
2181 from rc_reviewers.utils import validate_observers
2185 from rc_reviewers.utils import validate_observers
2182 except ImportError:
2186 except ImportError:
2183 from rhodecode.apps.repository.utils import get_default_reviewers_data
2187 from rhodecode.apps.repository.utils import get_default_reviewers_data
2184 from rhodecode.apps.repository.utils import validate_default_reviewers
2188 from rhodecode.apps.repository.utils import validate_default_reviewers
2185 from rhodecode.apps.repository.utils import validate_observers
2189 from rhodecode.apps.repository.utils import validate_observers
2186
2190
2187 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2191 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2188
2192
2189
2193
2190 class MergeCheck(object):
2194 class MergeCheck(object):
2191 """
2195 """
2192 Perform Merge Checks and returns a check object which stores information
2196 Perform Merge Checks and returns a check object which stores information
2193 about merge errors, and merge conditions
2197 about merge errors, and merge conditions
2194 """
2198 """
2195 TODO_CHECK = 'todo'
2199 TODO_CHECK = 'todo'
2196 PERM_CHECK = 'perm'
2200 PERM_CHECK = 'perm'
2197 REVIEW_CHECK = 'review'
2201 REVIEW_CHECK = 'review'
2198 MERGE_CHECK = 'merge'
2202 MERGE_CHECK = 'merge'
2199 WIP_CHECK = 'wip'
2203 WIP_CHECK = 'wip'
2200
2204
2201 def __init__(self):
2205 def __init__(self):
2202 self.review_status = None
2206 self.review_status = None
2203 self.merge_possible = None
2207 self.merge_possible = None
2204 self.merge_msg = ''
2208 self.merge_msg = ''
2205 self.merge_response = None
2209 self.merge_response = None
2206 self.failed = None
2210 self.failed = None
2207 self.errors = []
2211 self.errors = []
2208 self.error_details = OrderedDict()
2212 self.error_details = OrderedDict()
2209 self.source_commit = AttributeDict()
2213 self.source_commit = AttributeDict()
2210 self.target_commit = AttributeDict()
2214 self.target_commit = AttributeDict()
2211 self.reviewers_count = 0
2215 self.reviewers_count = 0
2212 self.observers_count = 0
2216 self.observers_count = 0
2213
2217
2214 def __repr__(self):
2218 def __repr__(self):
2215 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2219 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2216 self.merge_possible, self.failed, self.errors)
2220 self.merge_possible, self.failed, self.errors)
2217
2221
2218 def push_error(self, error_type, message, error_key, details):
2222 def push_error(self, error_type, message, error_key, details):
2219 self.failed = True
2223 self.failed = True
2220 self.errors.append([error_type, message])
2224 self.errors.append([error_type, message])
2221 self.error_details[error_key] = dict(
2225 self.error_details[error_key] = dict(
2222 details=details,
2226 details=details,
2223 error_type=error_type,
2227 error_type=error_type,
2224 message=message
2228 message=message
2225 )
2229 )
2226
2230
2227 @classmethod
2231 @classmethod
2228 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2232 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2229 force_shadow_repo_refresh=False):
2233 force_shadow_repo_refresh=False):
2230 _ = translator
2234 _ = translator
2231 merge_check = cls()
2235 merge_check = cls()
2232
2236
2233 # title has WIP:
2237 # title has WIP:
2234 if pull_request.work_in_progress:
2238 if pull_request.work_in_progress:
2235 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2239 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2236
2240
2237 msg = _('WIP marker in title prevents from accidental merge.')
2241 msg = _('WIP marker in title prevents from accidental merge.')
2238 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2242 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2239 if fail_early:
2243 if fail_early:
2240 return merge_check
2244 return merge_check
2241
2245
2242 # permissions to merge
2246 # permissions to merge
2243 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2247 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2244 if not user_allowed_to_merge:
2248 if not user_allowed_to_merge:
2245 log.debug("MergeCheck: cannot merge, approval is pending.")
2249 log.debug("MergeCheck: cannot merge, approval is pending.")
2246
2250
2247 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2251 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2248 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2252 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2249 if fail_early:
2253 if fail_early:
2250 return merge_check
2254 return merge_check
2251
2255
2252 # permission to merge into the target branch
2256 # permission to merge into the target branch
2253 target_commit_id = pull_request.target_ref_parts.commit_id
2257 target_commit_id = pull_request.target_ref_parts.commit_id
2254 if pull_request.target_ref_parts.type == 'branch':
2258 if pull_request.target_ref_parts.type == 'branch':
2255 branch_name = pull_request.target_ref_parts.name
2259 branch_name = pull_request.target_ref_parts.name
2256 else:
2260 else:
2257 # for mercurial we can always figure out the branch from the commit
2261 # for mercurial we can always figure out the branch from the commit
2258 # in case of bookmark
2262 # in case of bookmark
2259 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2263 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2260 branch_name = target_commit.branch
2264 branch_name = target_commit.branch
2261
2265
2262 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2266 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2263 pull_request.target_repo.repo_name, branch_name)
2267 pull_request.target_repo.repo_name, branch_name)
2264 if branch_perm and branch_perm == 'branch.none':
2268 if branch_perm and branch_perm == 'branch.none':
2265 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2269 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2266 branch_name, rule)
2270 branch_name, rule)
2267 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2271 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2268 if fail_early:
2272 if fail_early:
2269 return merge_check
2273 return merge_check
2270
2274
2271 # review status, must be always present
2275 # review status, must be always present
2272 review_status = pull_request.calculated_review_status()
2276 review_status = pull_request.calculated_review_status()
2273 merge_check.review_status = review_status
2277 merge_check.review_status = review_status
2274 merge_check.reviewers_count = pull_request.reviewers_count
2278 merge_check.reviewers_count = pull_request.reviewers_count
2275 merge_check.observers_count = pull_request.observers_count
2279 merge_check.observers_count = pull_request.observers_count
2276
2280
2277 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2281 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2278 if not status_approved and merge_check.reviewers_count:
2282 if not status_approved and merge_check.reviewers_count:
2279 log.debug("MergeCheck: cannot merge, approval is pending.")
2283 log.debug("MergeCheck: cannot merge, approval is pending.")
2280 msg = _('Pull request reviewer approval is pending.')
2284 msg = _('Pull request reviewer approval is pending.')
2281
2285
2282 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2286 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2283
2287
2284 if fail_early:
2288 if fail_early:
2285 return merge_check
2289 return merge_check
2286
2290
2287 # left over TODOs
2291 # left over TODOs
2288 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2292 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2289 if todos:
2293 if todos:
2290 log.debug("MergeCheck: cannot merge, {} "
2294 log.debug("MergeCheck: cannot merge, {} "
2291 "unresolved TODOs left.".format(len(todos)))
2295 "unresolved TODOs left.".format(len(todos)))
2292
2296
2293 if len(todos) == 1:
2297 if len(todos) == 1:
2294 msg = _('Cannot merge, {} TODO still not resolved.').format(
2298 msg = _('Cannot merge, {} TODO still not resolved.').format(
2295 len(todos))
2299 len(todos))
2296 else:
2300 else:
2297 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2301 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2298 len(todos))
2302 len(todos))
2299
2303
2300 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2304 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2301
2305
2302 if fail_early:
2306 if fail_early:
2303 return merge_check
2307 return merge_check
2304
2308
2305 # merge possible, here is the filesystem simulation + shadow repo
2309 # merge possible, here is the filesystem simulation + shadow repo
2306 merge_response, merge_status, msg = PullRequestModel().merge_status(
2310 merge_response, merge_status, msg = PullRequestModel().merge_status(
2307 pull_request, translator=translator,
2311 pull_request, translator=translator,
2308 force_shadow_repo_refresh=force_shadow_repo_refresh)
2312 force_shadow_repo_refresh=force_shadow_repo_refresh)
2309
2313
2310 merge_check.merge_possible = merge_status
2314 merge_check.merge_possible = merge_status
2311 merge_check.merge_msg = msg
2315 merge_check.merge_msg = msg
2312 merge_check.merge_response = merge_response
2316 merge_check.merge_response = merge_response
2313
2317
2314 source_ref_id = pull_request.source_ref_parts.commit_id
2318 source_ref_id = pull_request.source_ref_parts.commit_id
2315 target_ref_id = pull_request.target_ref_parts.commit_id
2319 target_ref_id = pull_request.target_ref_parts.commit_id
2316
2320
2317 try:
2321 try:
2318 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2322 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2319 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2323 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2320 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2324 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2321 merge_check.source_commit.current_raw_id = source_commit.raw_id
2325 merge_check.source_commit.current_raw_id = source_commit.raw_id
2322 merge_check.source_commit.previous_raw_id = source_ref_id
2326 merge_check.source_commit.previous_raw_id = source_ref_id
2323
2327
2324 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2328 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2325 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2329 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2326 merge_check.target_commit.current_raw_id = target_commit.raw_id
2330 merge_check.target_commit.current_raw_id = target_commit.raw_id
2327 merge_check.target_commit.previous_raw_id = target_ref_id
2331 merge_check.target_commit.previous_raw_id = target_ref_id
2328 except (SourceRefMissing, TargetRefMissing):
2332 except (SourceRefMissing, TargetRefMissing):
2329 pass
2333 pass
2330
2334
2331 if not merge_status:
2335 if not merge_status:
2332 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2336 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2333 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2337 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2334
2338
2335 if fail_early:
2339 if fail_early:
2336 return merge_check
2340 return merge_check
2337
2341
2338 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2342 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2339 return merge_check
2343 return merge_check
2340
2344
2341 @classmethod
2345 @classmethod
2342 def get_merge_conditions(cls, pull_request, translator):
2346 def get_merge_conditions(cls, pull_request, translator):
2343 _ = translator
2347 _ = translator
2344 merge_details = {}
2348 merge_details = {}
2345
2349
2346 model = PullRequestModel()
2350 model = PullRequestModel()
2347 use_rebase = model._use_rebase_for_merging(pull_request)
2351 use_rebase = model._use_rebase_for_merging(pull_request)
2348
2352
2349 if use_rebase:
2353 if use_rebase:
2350 merge_details['merge_strategy'] = dict(
2354 merge_details['merge_strategy'] = dict(
2351 details={},
2355 details={},
2352 message=_('Merge strategy: rebase')
2356 message=_('Merge strategy: rebase')
2353 )
2357 )
2354 else:
2358 else:
2355 merge_details['merge_strategy'] = dict(
2359 merge_details['merge_strategy'] = dict(
2356 details={},
2360 details={},
2357 message=_('Merge strategy: explicit merge commit')
2361 message=_('Merge strategy: explicit merge commit')
2358 )
2362 )
2359
2363
2360 close_branch = model._close_branch_before_merging(pull_request)
2364 close_branch = model._close_branch_before_merging(pull_request)
2361 if close_branch:
2365 if close_branch:
2362 repo_type = pull_request.target_repo.repo_type
2366 repo_type = pull_request.target_repo.repo_type
2363 close_msg = ''
2367 close_msg = ''
2364 if repo_type == 'hg':
2368 if repo_type == 'hg':
2365 close_msg = _('Source branch will be closed before the merge.')
2369 close_msg = _('Source branch will be closed before the merge.')
2366 elif repo_type == 'git':
2370 elif repo_type == 'git':
2367 close_msg = _('Source branch will be deleted after the merge.')
2371 close_msg = _('Source branch will be deleted after the merge.')
2368
2372
2369 merge_details['close_branch'] = dict(
2373 merge_details['close_branch'] = dict(
2370 details={},
2374 details={},
2371 message=close_msg
2375 message=close_msg
2372 )
2376 )
2373
2377
2374 return merge_details
2378 return merge_details
2375
2379
2376
2380
2377 @dataclasses.dataclass
2381 @dataclasses.dataclass
2378 class ChangeTuple:
2382 class ChangeTuple:
2379 added: list
2383 added: list
2380 common: list
2384 common: list
2381 removed: list
2385 removed: list
2382 total: list
2386 total: list
2383
2387
2384
2388
2385 @dataclasses.dataclass
2389 @dataclasses.dataclass
2386 class FileChangeTuple:
2390 class FileChangeTuple:
2387 added: list
2391 added: list
2388 modified: list
2392 modified: list
2389 removed: list
2393 removed: list
@@ -1,888 +1,889
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 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 import os
19 import os
20 import re
20 import re
21 import logging
21 import logging
22 import time
22 import time
23 import functools
23 import functools
24 from collections import namedtuple
24 from collections import namedtuple
25
25
26 from pyramid.threadlocal import get_current_request
26 from pyramid.threadlocal import get_current_request
27
27
28 from rhodecode.lib import rc_cache
28 from rhodecode.lib import rc_cache
29 from rhodecode.lib.hash_utils import sha1_safe
29 from rhodecode.lib.hash_utils import sha1_safe
30 from rhodecode.lib.html_filters import sanitize_html
30 from rhodecode.lib.html_filters import sanitize_html
31 from rhodecode.lib.utils2 import (
31 from rhodecode.lib.utils2 import (
32 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
32 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
33 from rhodecode.lib.vcs.backends import base
33 from rhodecode.lib.vcs.backends import base
34 from rhodecode.lib.statsd_client import StatsdClient
34 from rhodecode.lib.statsd_client import StatsdClient
35 from rhodecode.model import BaseModel
35 from rhodecode.model import BaseModel
36 from rhodecode.model.db import (
36 from rhodecode.model.db import (
37 RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting)
37 RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting)
38 from rhodecode.model.meta import Session
38 from rhodecode.model.meta import Session
39
39
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 UiSetting = namedtuple(
44 UiSetting = namedtuple(
45 'UiSetting', ['section', 'key', 'value', 'active'])
45 'UiSetting', ['section', 'key', 'value', 'active'])
46
46
47 SOCIAL_PLUGINS_LIST = ['github', 'bitbucket', 'twitter', 'google']
47 SOCIAL_PLUGINS_LIST = ['github', 'bitbucket', 'twitter', 'google']
48
48
49
49
50 class SettingNotFound(Exception):
50 class SettingNotFound(Exception):
51 def __init__(self, setting_id):
51 def __init__(self, setting_id):
52 msg = f'Setting `{setting_id}` is not found'
52 msg = f'Setting `{setting_id}` is not found'
53 super().__init__(msg)
53 super().__init__(msg)
54
54
55
55
56 class SettingsModel(BaseModel):
56 class SettingsModel(BaseModel):
57 BUILTIN_HOOKS = (
57 BUILTIN_HOOKS = (
58 RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH,
58 RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH,
59 RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH,
59 RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH,
60 RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL,
60 RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL,
61 RhodeCodeUi.HOOK_PUSH_KEY,)
61 RhodeCodeUi.HOOK_PUSH_KEY,)
62 HOOKS_SECTION = 'hooks'
62 HOOKS_SECTION = 'hooks'
63
63
64 def __init__(self, sa=None, repo=None):
64 def __init__(self, sa=None, repo=None):
65 self.repo = repo
65 self.repo = repo
66 self.UiDbModel = RepoRhodeCodeUi if repo else RhodeCodeUi
66 self.UiDbModel = RepoRhodeCodeUi if repo else RhodeCodeUi
67 self.SettingsDbModel = (
67 self.SettingsDbModel = (
68 RepoRhodeCodeSetting if repo else RhodeCodeSetting)
68 RepoRhodeCodeSetting if repo else RhodeCodeSetting)
69 super().__init__(sa)
69 super().__init__(sa)
70
70
71 def get_keyname(self, key_name, prefix='rhodecode_'):
71 def get_keyname(self, key_name, prefix='rhodecode_'):
72 return f'{prefix}{key_name}'
72 return f'{prefix}{key_name}'
73
73
74 def get_ui_by_key(self, key):
74 def get_ui_by_key(self, key):
75 q = self.UiDbModel.query()
75 q = self.UiDbModel.query()
76 q = q.filter(self.UiDbModel.ui_key == key)
76 q = q.filter(self.UiDbModel.ui_key == key)
77 q = self._filter_by_repo(RepoRhodeCodeUi, q)
77 q = self._filter_by_repo(RepoRhodeCodeUi, q)
78 return q.scalar()
78 return q.scalar()
79
79
80 def get_ui_by_section(self, section):
80 def get_ui_by_section(self, section):
81 q = self.UiDbModel.query()
81 q = self.UiDbModel.query()
82 q = q.filter(self.UiDbModel.ui_section == section)
82 q = q.filter(self.UiDbModel.ui_section == section)
83 q = self._filter_by_repo(RepoRhodeCodeUi, q)
83 q = self._filter_by_repo(RepoRhodeCodeUi, q)
84 return q.all()
84 return q.all()
85
85
86 def get_ui_by_section_and_key(self, section, key):
86 def get_ui_by_section_and_key(self, section, key):
87 q = self.UiDbModel.query()
87 q = self.UiDbModel.query()
88 q = q.filter(self.UiDbModel.ui_section == section)
88 q = q.filter(self.UiDbModel.ui_section == section)
89 q = q.filter(self.UiDbModel.ui_key == key)
89 q = q.filter(self.UiDbModel.ui_key == key)
90 q = self._filter_by_repo(RepoRhodeCodeUi, q)
90 q = self._filter_by_repo(RepoRhodeCodeUi, q)
91 return q.scalar()
91 return q.scalar()
92
92
93 def get_ui(self, section=None, key=None):
93 def get_ui(self, section=None, key=None):
94 q = self.UiDbModel.query()
94 q = self.UiDbModel.query()
95 q = self._filter_by_repo(RepoRhodeCodeUi, q)
95 q = self._filter_by_repo(RepoRhodeCodeUi, q)
96
96
97 if section:
97 if section:
98 q = q.filter(self.UiDbModel.ui_section == section)
98 q = q.filter(self.UiDbModel.ui_section == section)
99 if key:
99 if key:
100 q = q.filter(self.UiDbModel.ui_key == key)
100 q = q.filter(self.UiDbModel.ui_key == key)
101
101
102 # TODO: mikhail: add caching
102 # TODO: mikhail: add caching
103 result = [
103 result = [
104 UiSetting(
104 UiSetting(
105 section=safe_str(r.ui_section), key=safe_str(r.ui_key),
105 section=safe_str(r.ui_section), key=safe_str(r.ui_key),
106 value=safe_str(r.ui_value), active=r.ui_active
106 value=safe_str(r.ui_value), active=r.ui_active
107 )
107 )
108 for r in q.all()
108 for r in q.all()
109 ]
109 ]
110 return result
110 return result
111
111
112 def get_builtin_hooks(self):
112 def get_builtin_hooks(self):
113 q = self.UiDbModel.query()
113 q = self.UiDbModel.query()
114 q = q.filter(self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
114 q = q.filter(self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
115 return self._get_hooks(q)
115 return self._get_hooks(q)
116
116
117 def get_custom_hooks(self):
117 def get_custom_hooks(self):
118 q = self.UiDbModel.query()
118 q = self.UiDbModel.query()
119 q = q.filter(~self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
119 q = q.filter(~self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
120 return self._get_hooks(q)
120 return self._get_hooks(q)
121
121
122 def create_ui_section_value(self, section, val, key=None, active=True):
122 def create_ui_section_value(self, section, val, key=None, active=True):
123 new_ui = self.UiDbModel()
123 new_ui = self.UiDbModel()
124 new_ui.ui_section = section
124 new_ui.ui_section = section
125 new_ui.ui_value = val
125 new_ui.ui_value = val
126 new_ui.ui_active = active
126 new_ui.ui_active = active
127
127
128 repository_id = ''
128 repository_id = ''
129 if self.repo:
129 if self.repo:
130 repo = self._get_repo(self.repo)
130 repo = self._get_repo(self.repo)
131 repository_id = repo.repo_id
131 repository_id = repo.repo_id
132 new_ui.repository_id = repository_id
132 new_ui.repository_id = repository_id
133
133
134 if not key:
134 if not key:
135 # keys are unique so they need appended info
135 # keys are unique so they need appended info
136 if self.repo:
136 if self.repo:
137 key = sha1_safe(f'{section}{val}{repository_id}')
137 key = sha1_safe(f'{section}{val}{repository_id}')
138 else:
138 else:
139 key = sha1_safe(f'{section}{val}')
139 key = sha1_safe(f'{section}{val}')
140
140
141 new_ui.ui_key = key
141 new_ui.ui_key = key
142
142
143 Session().add(new_ui)
143 Session().add(new_ui)
144 return new_ui
144 return new_ui
145
145
146 def create_or_update_hook(self, key, value):
146 def create_or_update_hook(self, key, value):
147 ui = (
147 ui = (
148 self.get_ui_by_section_and_key(self.HOOKS_SECTION, key) or
148 self.get_ui_by_section_and_key(self.HOOKS_SECTION, key) or
149 self.UiDbModel())
149 self.UiDbModel())
150 ui.ui_section = self.HOOKS_SECTION
150 ui.ui_section = self.HOOKS_SECTION
151 ui.ui_active = True
151 ui.ui_active = True
152 ui.ui_key = key
152 ui.ui_key = key
153 ui.ui_value = value
153 ui.ui_value = value
154
154
155 if self.repo:
155 if self.repo:
156 repo = self._get_repo(self.repo)
156 repo = self._get_repo(self.repo)
157 repository_id = repo.repo_id
157 repository_id = repo.repo_id
158 ui.repository_id = repository_id
158 ui.repository_id = repository_id
159
159
160 Session().add(ui)
160 Session().add(ui)
161 return ui
161 return ui
162
162
163 def delete_ui(self, id_):
163 def delete_ui(self, id_):
164 ui = self.UiDbModel.get(id_)
164 ui = self.UiDbModel.get(id_)
165 if not ui:
165 if not ui:
166 raise SettingNotFound(id_)
166 raise SettingNotFound(id_)
167 Session().delete(ui)
167 Session().delete(ui)
168
168
169 def get_setting_by_name(self, name):
169 def get_setting_by_name(self, name):
170 q = self._get_settings_query()
170 q = self._get_settings_query()
171 q = q.filter(self.SettingsDbModel.app_settings_name == name)
171 q = q.filter(self.SettingsDbModel.app_settings_name == name)
172 return q.scalar()
172 return q.scalar()
173
173
174 def create_or_update_setting(
174 def create_or_update_setting(
175 self, name, val: Optional | str = Optional(''), type_: Optional | str = Optional('unicode')):
175 self, name, val: Optional | str = Optional(''), type_: Optional | str = Optional('unicode')):
176 """
176 """
177 Creates or updates RhodeCode setting. If updates are triggered, it will
177 Creates or updates RhodeCode setting. If updates are triggered, it will
178 only update parameters that are explicitly set Optional instance will
178 only update parameters that are explicitly set Optional instance will
179 be skipped
179 be skipped
180
180
181 :param name:
181 :param name:
182 :param val:
182 :param val:
183 :param type_:
183 :param type_:
184 :return:
184 :return:
185 """
185 """
186
186
187 res = self.get_setting_by_name(name)
187 res = self.get_setting_by_name(name)
188 repo = self._get_repo(self.repo) if self.repo else None
188 repo = self._get_repo(self.repo) if self.repo else None
189
189
190 if not res:
190 if not res:
191 val = Optional.extract(val)
191 val = Optional.extract(val)
192 type_ = Optional.extract(type_)
192 type_ = Optional.extract(type_)
193
193
194 args = (
194 args = (
195 (repo.repo_id, name, val, type_)
195 (repo.repo_id, name, val, type_)
196 if repo else (name, val, type_))
196 if repo else (name, val, type_))
197 res = self.SettingsDbModel(*args)
197 res = self.SettingsDbModel(*args)
198
198
199 else:
199 else:
200 if self.repo:
200 if self.repo:
201 res.repository_id = repo.repo_id
201 res.repository_id = repo.repo_id
202
202
203 res.app_settings_name = name
203 res.app_settings_name = name
204 if not isinstance(type_, Optional):
204 if not isinstance(type_, Optional):
205 # update if set
205 # update if set
206 res.app_settings_type = type_
206 res.app_settings_type = type_
207 if not isinstance(val, Optional):
207 if not isinstance(val, Optional):
208 # update if set
208 # update if set
209 res.app_settings_value = val
209 res.app_settings_value = val
210
210
211 Session().add(res)
211 Session().add(res)
212 return res
212 return res
213
213
214 def get_cache_region(self):
214 def get_cache_region(self):
215 repo = self._get_repo(self.repo) if self.repo else None
215 repo = self._get_repo(self.repo) if self.repo else None
216 cache_key = f"repo.v1.{repo.repo_id}" if repo else "repo.v1.ALL"
216 cache_key = f"repo.v1.{repo.repo_id}" if repo else "repo.v1.ALL"
217 cache_namespace_uid = f'cache_settings.{cache_key}'
217 cache_namespace_uid = f'cache_settings.{cache_key}'
218 region = rc_cache.get_or_create_region('cache_general', cache_namespace_uid)
218 region = rc_cache.get_or_create_region('cache_general', cache_namespace_uid)
219 return region, cache_namespace_uid
219 return region, cache_namespace_uid
220
220
221 def invalidate_settings_cache(self, hard=False):
221 def invalidate_settings_cache(self, hard=False):
222 region, namespace_key = self.get_cache_region()
222 region, namespace_key = self.get_cache_region()
223 log.debug('Invalidation cache [%s] region %s for cache_key: %s',
223 log.debug('Invalidation cache [%s] region %s for cache_key: %s',
224 'invalidate_settings_cache', region, namespace_key)
224 'invalidate_settings_cache', region, namespace_key)
225
225
226 # we use hard cleanup if invalidation is sent
226 # we use hard cleanup if invalidation is sent
227 rc_cache.clear_cache_namespace(region, namespace_key, method=rc_cache.CLEAR_DELETE)
227 rc_cache.clear_cache_namespace(region, namespace_key, method=rc_cache.CLEAR_DELETE)
228
228
229 def get_cache_call_method(self, cache=True):
229 def get_cache_call_method(self, cache=True):
230 region, cache_key = self.get_cache_region()
230 region, cache_key = self.get_cache_region()
231
231
232 @region.conditional_cache_on_arguments(condition=cache)
232 @region.conditional_cache_on_arguments(condition=cache)
233 def _get_all_settings(name, key):
233 def _get_all_settings(name, key):
234 q = self._get_settings_query()
234 q = self._get_settings_query()
235 if not q:
235 if not q:
236 raise Exception('Could not get application settings !')
236 raise Exception('Could not get application settings !')
237
237
238 settings = {
238 settings = {
239 self.get_keyname(res.app_settings_name): res.app_settings_value
239 self.get_keyname(res.app_settings_name): res.app_settings_value
240 for res in q
240 for res in q
241 }
241 }
242 return settings
242 return settings
243 return _get_all_settings
243 return _get_all_settings
244
244
245 def get_all_settings(self, cache=False, from_request=True):
245 def get_all_settings(self, cache=False, from_request=True):
246 # defines if we use GLOBAL, or PER_REPO
246 # defines if we use GLOBAL, or PER_REPO
247 repo = self._get_repo(self.repo) if self.repo else None
247 repo = self._get_repo(self.repo) if self.repo else None
248
248
249 # initially try the request context; this is the fastest
249 # initially try the request context; this is the fastest
250 # we only fetch global config, NOT for repo-specific
250 # we only fetch global config, NOT for repo-specific
251 if from_request and not repo:
251 if from_request and not repo:
252 request = get_current_request()
252 request = get_current_request()
253
253
254 if request and hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
254 if request and hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
255 rc_config = request.call_context.rc_config
255 rc_config = request.call_context.rc_config
256 if rc_config:
256 if rc_config:
257 return rc_config
257 return rc_config
258
258
259 _region, cache_key = self.get_cache_region()
259 _region, cache_key = self.get_cache_region()
260 _get_all_settings = self.get_cache_call_method(cache=cache)
260 _get_all_settings = self.get_cache_call_method(cache=cache)
261
261
262 start = time.time()
262 start = time.time()
263 result = _get_all_settings('rhodecode_settings', cache_key)
263 result = _get_all_settings('rhodecode_settings', cache_key)
264 compute_time = time.time() - start
264 compute_time = time.time() - start
265 log.debug('cached method:%s took %.4fs', _get_all_settings.__name__, compute_time)
265 log.debug('cached method:%s took %.4fs', _get_all_settings.__name__, compute_time)
266
266
267 statsd = StatsdClient.statsd
267 statsd = StatsdClient.statsd
268 if statsd:
268 if statsd:
269 elapsed_time_ms = round(1000.0 * compute_time) # use ms only
269 elapsed_time_ms = round(1000.0 * compute_time) # use ms only
270 statsd.timing("rhodecode_settings_timing.histogram", elapsed_time_ms,
270 statsd.timing("rhodecode_settings_timing.histogram", elapsed_time_ms,
271 use_decimals=False)
271 use_decimals=False)
272
272
273 log.debug('Fetching app settings for key: %s took: %.4fs: cache: %s', cache_key, compute_time, cache)
273 log.debug('Fetching app settings for key: %s took: %.4fs: cache: %s', cache_key, compute_time, cache)
274
274
275 return result
275 return result
276
276
277 def get_auth_settings(self):
277 def get_auth_settings(self):
278 q = self._get_settings_query()
278 q = self._get_settings_query()
279 q = q.filter(
279 q = q.filter(
280 self.SettingsDbModel.app_settings_name.startswith('auth_'))
280 self.SettingsDbModel.app_settings_name.startswith('auth_'))
281 rows = q.all()
281 rows = q.all()
282 auth_settings = {
282 auth_settings = {
283 row.app_settings_name: row.app_settings_value for row in rows}
283 row.app_settings_name: row.app_settings_value for row in rows}
284 return auth_settings
284 return auth_settings
285
285
286 def get_auth_plugins(self):
286 def get_auth_plugins(self):
287 auth_plugins = self.get_setting_by_name("auth_plugins")
287 auth_plugins = self.get_setting_by_name("auth_plugins")
288 return auth_plugins.app_settings_value
288 return auth_plugins.app_settings_value
289
289
290 def get_default_repo_settings(self, strip_prefix=False):
290 def get_default_repo_settings(self, strip_prefix=False):
291 q = self._get_settings_query()
291 q = self._get_settings_query()
292 q = q.filter(
292 q = q.filter(
293 self.SettingsDbModel.app_settings_name.startswith('default_'))
293 self.SettingsDbModel.app_settings_name.startswith('default_'))
294 rows = q.all()
294 rows = q.all()
295
295
296 result = {}
296 result = {}
297 for row in rows:
297 for row in rows:
298 key = row.app_settings_name
298 key = row.app_settings_name
299 if strip_prefix:
299 if strip_prefix:
300 key = remove_prefix(key, prefix='default_')
300 key = remove_prefix(key, prefix='default_')
301 result.update({key: row.app_settings_value})
301 result.update({key: row.app_settings_value})
302 return result
302 return result
303
303
304 def get_repo(self):
304 def get_repo(self):
305 repo = self._get_repo(self.repo)
305 repo = self._get_repo(self.repo)
306 if not repo:
306 if not repo:
307 raise Exception(
307 raise Exception(
308 f'Repository `{self.repo}` cannot be found inside the database')
308 f'Repository `{self.repo}` cannot be found inside the database')
309 return repo
309 return repo
310
310
311 def _filter_by_repo(self, model, query):
311 def _filter_by_repo(self, model, query):
312 if self.repo:
312 if self.repo:
313 repo = self.get_repo()
313 repo = self.get_repo()
314 query = query.filter(model.repository_id == repo.repo_id)
314 query = query.filter(model.repository_id == repo.repo_id)
315 return query
315 return query
316
316
317 def _get_hooks(self, query):
317 def _get_hooks(self, query):
318 query = query.filter(self.UiDbModel.ui_section == self.HOOKS_SECTION)
318 query = query.filter(self.UiDbModel.ui_section == self.HOOKS_SECTION)
319 query = self._filter_by_repo(RepoRhodeCodeUi, query)
319 query = self._filter_by_repo(RepoRhodeCodeUi, query)
320 return query.all()
320 return query.all()
321
321
322 def _get_settings_query(self):
322 def _get_settings_query(self):
323 q = self.SettingsDbModel.query()
323 q = self.SettingsDbModel.query()
324 return self._filter_by_repo(RepoRhodeCodeSetting, q)
324 return self._filter_by_repo(RepoRhodeCodeSetting, q)
325
325
326 def list_enabled_social_plugins(self, settings):
326 def list_enabled_social_plugins(self, settings):
327 enabled = []
327 enabled = []
328 for plug in SOCIAL_PLUGINS_LIST:
328 for plug in SOCIAL_PLUGINS_LIST:
329 if str2bool(settings.get(f'rhodecode_auth_{plug}_enabled')):
329 if str2bool(settings.get(f'rhodecode_auth_{plug}_enabled')):
330 enabled.append(plug)
330 enabled.append(plug)
331 return enabled
331 return enabled
332
332
333
333
334 def assert_repo_settings(func):
334 def assert_repo_settings(func):
335 @functools.wraps(func)
335 @functools.wraps(func)
336 def _wrapper(self, *args, **kwargs):
336 def _wrapper(self, *args, **kwargs):
337 if not self.repo_settings:
337 if not self.repo_settings:
338 raise Exception('Repository is not specified')
338 raise Exception('Repository is not specified')
339 return func(self, *args, **kwargs)
339 return func(self, *args, **kwargs)
340 return _wrapper
340 return _wrapper
341
341
342
342
343 class IssueTrackerSettingsModel(object):
343 class IssueTrackerSettingsModel(object):
344 INHERIT_SETTINGS = 'inherit_issue_tracker_settings'
344 INHERIT_SETTINGS = 'inherit_issue_tracker_settings'
345 SETTINGS_PREFIX = 'issuetracker_'
345 SETTINGS_PREFIX = 'issuetracker_'
346
346
347 def __init__(self, sa=None, repo=None):
347 def __init__(self, sa=None, repo=None):
348 self.global_settings = SettingsModel(sa=sa)
348 self.global_settings = SettingsModel(sa=sa)
349 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
349 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
350
350
351 @property
351 @property
352 def inherit_global_settings(self):
352 def inherit_global_settings(self):
353 if not self.repo_settings:
353 if not self.repo_settings:
354 return True
354 return True
355 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
355 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
356 return setting.app_settings_value if setting else True
356 return setting.app_settings_value if setting else True
357
357
358 @inherit_global_settings.setter
358 @inherit_global_settings.setter
359 def inherit_global_settings(self, value):
359 def inherit_global_settings(self, value):
360 if self.repo_settings:
360 if self.repo_settings:
361 settings = self.repo_settings.create_or_update_setting(
361 settings = self.repo_settings.create_or_update_setting(
362 self.INHERIT_SETTINGS, value, type_='bool')
362 self.INHERIT_SETTINGS, value, type_='bool')
363 Session().add(settings)
363 Session().add(settings)
364
364
365 def _get_keyname(self, key, uid, prefix='rhodecode_'):
365 def _get_keyname(self, key, uid, prefix='rhodecode_'):
366 return f'{prefix}{self.SETTINGS_PREFIX}{key}_{uid}'
366 return f'{prefix}{self.SETTINGS_PREFIX}{key}_{uid}'
367
367
368 def _make_dict_for_settings(self, qs):
368 def _make_dict_for_settings(self, qs):
369 prefix_match = self._get_keyname('pat', '',)
369 prefix_match = self._get_keyname('pat', '',)
370
370
371 issuetracker_entries = {}
371 issuetracker_entries = {}
372 # create keys
372 # create keys
373 for k, v in qs.items():
373 for k, v in qs.items():
374 if k.startswith(prefix_match):
374 if k.startswith(prefix_match):
375 uid = k[len(prefix_match):]
375 uid = k[len(prefix_match):]
376 issuetracker_entries[uid] = None
376 issuetracker_entries[uid] = None
377
377
378 def url_cleaner(input_str):
378 def url_cleaner(input_str):
379 input_str = input_str.replace('"', '').replace("'", '')
379 input_str = input_str.replace('"', '').replace("'", '')
380 input_str = sanitize_html(input_str, strip=True)
380 input_str = sanitize_html(input_str, strip=True)
381 return input_str
381 return input_str
382
382
383 # populate
383 # populate
384 for uid in issuetracker_entries:
384 for uid in issuetracker_entries:
385 url_data = qs.get(self._get_keyname('url', uid))
385 url_data = qs.get(self._get_keyname('url', uid))
386
386
387 pat = qs.get(self._get_keyname('pat', uid))
387 pat = qs.get(self._get_keyname('pat', uid))
388 try:
388 try:
389 pat_compiled = re.compile(r'%s' % pat)
389 pat_compiled = re.compile(r'%s' % pat)
390 except re.error:
390 except re.error:
391 pat_compiled = None
391 pat_compiled = None
392
392
393 issuetracker_entries[uid] = AttributeDict({
393 issuetracker_entries[uid] = AttributeDict({
394 'pat': pat,
394 'pat': pat,
395 'pat_compiled': pat_compiled,
395 'pat_compiled': pat_compiled,
396 'url': url_cleaner(
396 'url': url_cleaner(
397 qs.get(self._get_keyname('url', uid)) or ''),
397 qs.get(self._get_keyname('url', uid)) or ''),
398 'pref': sanitize_html(
398 'pref': sanitize_html(
399 qs.get(self._get_keyname('pref', uid)) or ''),
399 qs.get(self._get_keyname('pref', uid)) or ''),
400 'desc': qs.get(
400 'desc': qs.get(
401 self._get_keyname('desc', uid)),
401 self._get_keyname('desc', uid)),
402 })
402 })
403
403
404 return issuetracker_entries
404 return issuetracker_entries
405
405
406 def get_global_settings(self, cache=False):
406 def get_global_settings(self, cache=False):
407 """
407 """
408 Returns list of global issue tracker settings
408 Returns list of global issue tracker settings
409 """
409 """
410 defaults = self.global_settings.get_all_settings(cache=cache)
410 defaults = self.global_settings.get_all_settings(cache=cache)
411 settings = self._make_dict_for_settings(defaults)
411 settings = self._make_dict_for_settings(defaults)
412 return settings
412 return settings
413
413
414 def get_repo_settings(self, cache=False):
414 def get_repo_settings(self, cache=False):
415 """
415 """
416 Returns list of issue tracker settings per repository
416 Returns list of issue tracker settings per repository
417 """
417 """
418 if not self.repo_settings:
418 if not self.repo_settings:
419 raise Exception('Repository is not specified')
419 raise Exception('Repository is not specified')
420 all_settings = self.repo_settings.get_all_settings(cache=cache)
420 all_settings = self.repo_settings.get_all_settings(cache=cache)
421 settings = self._make_dict_for_settings(all_settings)
421 settings = self._make_dict_for_settings(all_settings)
422 return settings
422 return settings
423
423
424 def get_settings(self, cache=False):
424 def get_settings(self, cache=False):
425 if self.inherit_global_settings:
425 if self.inherit_global_settings:
426 return self.get_global_settings(cache=cache)
426 return self.get_global_settings(cache=cache)
427 else:
427 else:
428 return self.get_repo_settings(cache=cache)
428 return self.get_repo_settings(cache=cache)
429
429
430 def delete_entries(self, uid):
430 def delete_entries(self, uid):
431 if self.repo_settings:
431 if self.repo_settings:
432 all_patterns = self.get_repo_settings()
432 all_patterns = self.get_repo_settings()
433 settings_model = self.repo_settings
433 settings_model = self.repo_settings
434 else:
434 else:
435 all_patterns = self.get_global_settings()
435 all_patterns = self.get_global_settings()
436 settings_model = self.global_settings
436 settings_model = self.global_settings
437 entries = all_patterns.get(uid, [])
437 entries = all_patterns.get(uid, [])
438
438
439 for del_key in entries:
439 for del_key in entries:
440 setting_name = self._get_keyname(del_key, uid, prefix='')
440 setting_name = self._get_keyname(del_key, uid, prefix='')
441 entry = settings_model.get_setting_by_name(setting_name)
441 entry = settings_model.get_setting_by_name(setting_name)
442 if entry:
442 if entry:
443 Session().delete(entry)
443 Session().delete(entry)
444
444
445 Session().commit()
445 Session().commit()
446
446
447 def create_or_update_setting(
447 def create_or_update_setting(
448 self, name, val=Optional(''), type_=Optional('unicode')):
448 self, name, val=Optional(''), type_=Optional('unicode')):
449 if self.repo_settings:
449 if self.repo_settings:
450 setting = self.repo_settings.create_or_update_setting(
450 setting = self.repo_settings.create_or_update_setting(
451 name, val, type_)
451 name, val, type_)
452 else:
452 else:
453 setting = self.global_settings.create_or_update_setting(
453 setting = self.global_settings.create_or_update_setting(
454 name, val, type_)
454 name, val, type_)
455 return setting
455 return setting
456
456
457
457
458 class VcsSettingsModel(object):
458 class VcsSettingsModel(object):
459
459
460 INHERIT_SETTINGS = 'inherit_vcs_settings'
460 INHERIT_SETTINGS = 'inherit_vcs_settings'
461 GENERAL_SETTINGS = (
461 GENERAL_SETTINGS = (
462 'use_outdated_comments',
462 'use_outdated_comments',
463 'pr_merge_enabled',
463 'pr_merge_enabled',
464 'auto_merge_enabled',
464 'hg_use_rebase_for_merging',
465 'hg_use_rebase_for_merging',
465 'hg_close_branch_before_merging',
466 'hg_close_branch_before_merging',
466 'git_use_rebase_for_merging',
467 'git_use_rebase_for_merging',
467 'git_close_branch_before_merging',
468 'git_close_branch_before_merging',
468 'diff_cache',
469 'diff_cache',
469 )
470 )
470
471
471 HOOKS_SETTINGS = (
472 HOOKS_SETTINGS = (
472 ('hooks', 'changegroup.repo_size'),
473 ('hooks', 'changegroup.repo_size'),
473 ('hooks', 'changegroup.push_logger'),
474 ('hooks', 'changegroup.push_logger'),
474 ('hooks', 'outgoing.pull_logger'),
475 ('hooks', 'outgoing.pull_logger'),
475 )
476 )
476 HG_SETTINGS = (
477 HG_SETTINGS = (
477 ('extensions', 'largefiles'),
478 ('extensions', 'largefiles'),
478 ('phases', 'publish'),
479 ('phases', 'publish'),
479 ('extensions', 'evolve'),
480 ('extensions', 'evolve'),
480 ('extensions', 'topic'),
481 ('extensions', 'topic'),
481 ('experimental', 'evolution'),
482 ('experimental', 'evolution'),
482 ('experimental', 'evolution.exchange'),
483 ('experimental', 'evolution.exchange'),
483 )
484 )
484 GIT_SETTINGS = (
485 GIT_SETTINGS = (
485 ('vcs_git_lfs', 'enabled'),
486 ('vcs_git_lfs', 'enabled'),
486 )
487 )
487 GLOBAL_HG_SETTINGS = (
488 GLOBAL_HG_SETTINGS = (
488 ('extensions', 'largefiles'),
489 ('extensions', 'largefiles'),
489 ('phases', 'publish'),
490 ('phases', 'publish'),
490 ('extensions', 'evolve'),
491 ('extensions', 'evolve'),
491 ('extensions', 'topic'),
492 ('extensions', 'topic'),
492 ('experimental', 'evolution'),
493 ('experimental', 'evolution'),
493 ('experimental', 'evolution.exchange'),
494 ('experimental', 'evolution.exchange'),
494 )
495 )
495
496
496 GLOBAL_GIT_SETTINGS = (
497 GLOBAL_GIT_SETTINGS = (
497 ('vcs_git_lfs', 'enabled'),
498 ('vcs_git_lfs', 'enabled'),
498 )
499 )
499
500
500 SVN_BRANCH_SECTION = 'vcs_svn_branch'
501 SVN_BRANCH_SECTION = 'vcs_svn_branch'
501 SVN_TAG_SECTION = 'vcs_svn_tag'
502 SVN_TAG_SECTION = 'vcs_svn_tag'
502 PATH_SETTING = ('paths', '/')
503 PATH_SETTING = ('paths', '/')
503
504
504 def __init__(self, sa=None, repo=None):
505 def __init__(self, sa=None, repo=None):
505 self.global_settings = SettingsModel(sa=sa)
506 self.global_settings = SettingsModel(sa=sa)
506 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
507 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
507 self._ui_settings = (
508 self._ui_settings = (
508 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
509 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
509 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
510 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
510
511
511 @property
512 @property
512 @assert_repo_settings
513 @assert_repo_settings
513 def inherit_global_settings(self):
514 def inherit_global_settings(self):
514 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
515 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
515 return setting.app_settings_value if setting else True
516 return setting.app_settings_value if setting else True
516
517
517 @inherit_global_settings.setter
518 @inherit_global_settings.setter
518 @assert_repo_settings
519 @assert_repo_settings
519 def inherit_global_settings(self, value):
520 def inherit_global_settings(self, value):
520 self.repo_settings.create_or_update_setting(
521 self.repo_settings.create_or_update_setting(
521 self.INHERIT_SETTINGS, value, type_='bool')
522 self.INHERIT_SETTINGS, value, type_='bool')
522
523
523 def get_keyname(self, key_name, prefix='rhodecode_'):
524 def get_keyname(self, key_name, prefix='rhodecode_'):
524 return f'{prefix}{key_name}'
525 return f'{prefix}{key_name}'
525
526
526 def get_global_svn_branch_patterns(self):
527 def get_global_svn_branch_patterns(self):
527 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
528 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
528
529
529 @assert_repo_settings
530 @assert_repo_settings
530 def get_repo_svn_branch_patterns(self):
531 def get_repo_svn_branch_patterns(self):
531 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
532 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
532
533
533 def get_global_svn_tag_patterns(self):
534 def get_global_svn_tag_patterns(self):
534 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
535 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
535
536
536 @assert_repo_settings
537 @assert_repo_settings
537 def get_repo_svn_tag_patterns(self):
538 def get_repo_svn_tag_patterns(self):
538 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
539 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
539
540
540 def get_global_settings(self):
541 def get_global_settings(self):
541 return self._collect_all_settings(global_=True)
542 return self._collect_all_settings(global_=True)
542
543
543 @assert_repo_settings
544 @assert_repo_settings
544 def get_repo_settings(self):
545 def get_repo_settings(self):
545 return self._collect_all_settings(global_=False)
546 return self._collect_all_settings(global_=False)
546
547
547 @assert_repo_settings
548 @assert_repo_settings
548 def get_repo_settings_inherited(self):
549 def get_repo_settings_inherited(self):
549 global_settings = self.get_global_settings()
550 global_settings = self.get_global_settings()
550 global_settings.update(self.get_repo_settings())
551 global_settings.update(self.get_repo_settings())
551 return global_settings
552 return global_settings
552
553
553 @assert_repo_settings
554 @assert_repo_settings
554 def create_or_update_repo_settings(
555 def create_or_update_repo_settings(
555 self, data, inherit_global_settings=False):
556 self, data, inherit_global_settings=False):
556 from rhodecode.model.scm import ScmModel
557 from rhodecode.model.scm import ScmModel
557
558
558 self.inherit_global_settings = inherit_global_settings
559 self.inherit_global_settings = inherit_global_settings
559
560
560 repo = self.repo_settings.get_repo()
561 repo = self.repo_settings.get_repo()
561 if not inherit_global_settings:
562 if not inherit_global_settings:
562 if repo.repo_type == 'svn':
563 if repo.repo_type == 'svn':
563 self.create_repo_svn_settings(data)
564 self.create_repo_svn_settings(data)
564 else:
565 else:
565 self.create_or_update_repo_hook_settings(data)
566 self.create_or_update_repo_hook_settings(data)
566 self.create_or_update_repo_pr_settings(data)
567 self.create_or_update_repo_pr_settings(data)
567
568
568 if repo.repo_type == 'hg':
569 if repo.repo_type == 'hg':
569 self.create_or_update_repo_hg_settings(data)
570 self.create_or_update_repo_hg_settings(data)
570
571
571 if repo.repo_type == 'git':
572 if repo.repo_type == 'git':
572 self.create_or_update_repo_git_settings(data)
573 self.create_or_update_repo_git_settings(data)
573
574
574 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
575 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
575
576
576 @assert_repo_settings
577 @assert_repo_settings
577 def create_or_update_repo_hook_settings(self, data):
578 def create_or_update_repo_hook_settings(self, data):
578 for section, key in self.HOOKS_SETTINGS:
579 for section, key in self.HOOKS_SETTINGS:
579 data_key = self._get_form_ui_key(section, key)
580 data_key = self._get_form_ui_key(section, key)
580 if data_key not in data:
581 if data_key not in data:
581 raise ValueError(
582 raise ValueError(
582 f'The given data does not contain {data_key} key')
583 f'The given data does not contain {data_key} key')
583
584
584 active = data.get(data_key)
585 active = data.get(data_key)
585 repo_setting = self.repo_settings.get_ui_by_section_and_key(
586 repo_setting = self.repo_settings.get_ui_by_section_and_key(
586 section, key)
587 section, key)
587 if not repo_setting:
588 if not repo_setting:
588 global_setting = self.global_settings.\
589 global_setting = self.global_settings.\
589 get_ui_by_section_and_key(section, key)
590 get_ui_by_section_and_key(section, key)
590 self.repo_settings.create_ui_section_value(
591 self.repo_settings.create_ui_section_value(
591 section, global_setting.ui_value, key=key, active=active)
592 section, global_setting.ui_value, key=key, active=active)
592 else:
593 else:
593 repo_setting.ui_active = active
594 repo_setting.ui_active = active
594 Session().add(repo_setting)
595 Session().add(repo_setting)
595
596
596 def update_global_hook_settings(self, data):
597 def update_global_hook_settings(self, data):
597 for section, key in self.HOOKS_SETTINGS:
598 for section, key in self.HOOKS_SETTINGS:
598 data_key = self._get_form_ui_key(section, key)
599 data_key = self._get_form_ui_key(section, key)
599 if data_key not in data:
600 if data_key not in data:
600 raise ValueError(
601 raise ValueError(
601 f'The given data does not contain {data_key} key')
602 f'The given data does not contain {data_key} key')
602 active = data.get(data_key)
603 active = data.get(data_key)
603 repo_setting = self.global_settings.get_ui_by_section_and_key(
604 repo_setting = self.global_settings.get_ui_by_section_and_key(
604 section, key)
605 section, key)
605 repo_setting.ui_active = active
606 repo_setting.ui_active = active
606 Session().add(repo_setting)
607 Session().add(repo_setting)
607
608
608 @assert_repo_settings
609 @assert_repo_settings
609 def create_or_update_repo_pr_settings(self, data):
610 def create_or_update_repo_pr_settings(self, data):
610 return self._create_or_update_general_settings(
611 return self._create_or_update_general_settings(
611 self.repo_settings, data)
612 self.repo_settings, data)
612
613
613 def create_or_update_global_pr_settings(self, data):
614 def create_or_update_global_pr_settings(self, data):
614 return self._create_or_update_general_settings(
615 return self._create_or_update_general_settings(
615 self.global_settings, data)
616 self.global_settings, data)
616
617
617 @assert_repo_settings
618 @assert_repo_settings
618 def create_repo_svn_settings(self, data):
619 def create_repo_svn_settings(self, data):
619 return self._create_svn_settings(self.repo_settings, data)
620 return self._create_svn_settings(self.repo_settings, data)
620
621
621 def _set_evolution(self, settings, is_enabled):
622 def _set_evolution(self, settings, is_enabled):
622 if is_enabled:
623 if is_enabled:
623 # if evolve is active set evolution=all
624 # if evolve is active set evolution=all
624
625
625 self._create_or_update_ui(
626 self._create_or_update_ui(
626 settings, *('experimental', 'evolution'), value='all',
627 settings, *('experimental', 'evolution'), value='all',
627 active=True)
628 active=True)
628 self._create_or_update_ui(
629 self._create_or_update_ui(
629 settings, *('experimental', 'evolution.exchange'), value='yes',
630 settings, *('experimental', 'evolution.exchange'), value='yes',
630 active=True)
631 active=True)
631 # if evolve is active set topics server support
632 # if evolve is active set topics server support
632 self._create_or_update_ui(
633 self._create_or_update_ui(
633 settings, *('extensions', 'topic'), value='',
634 settings, *('extensions', 'topic'), value='',
634 active=True)
635 active=True)
635
636
636 else:
637 else:
637 self._create_or_update_ui(
638 self._create_or_update_ui(
638 settings, *('experimental', 'evolution'), value='',
639 settings, *('experimental', 'evolution'), value='',
639 active=False)
640 active=False)
640 self._create_or_update_ui(
641 self._create_or_update_ui(
641 settings, *('experimental', 'evolution.exchange'), value='no',
642 settings, *('experimental', 'evolution.exchange'), value='no',
642 active=False)
643 active=False)
643 self._create_or_update_ui(
644 self._create_or_update_ui(
644 settings, *('extensions', 'topic'), value='',
645 settings, *('extensions', 'topic'), value='',
645 active=False)
646 active=False)
646
647
647 @assert_repo_settings
648 @assert_repo_settings
648 def create_or_update_repo_hg_settings(self, data):
649 def create_or_update_repo_hg_settings(self, data):
649 largefiles, phases, evolve = \
650 largefiles, phases, evolve = \
650 self.HG_SETTINGS[:3]
651 self.HG_SETTINGS[:3]
651 largefiles_key, phases_key, evolve_key = \
652 largefiles_key, phases_key, evolve_key = \
652 self._get_settings_keys(self.HG_SETTINGS[:3], data)
653 self._get_settings_keys(self.HG_SETTINGS[:3], data)
653
654
654 self._create_or_update_ui(
655 self._create_or_update_ui(
655 self.repo_settings, *largefiles, value='',
656 self.repo_settings, *largefiles, value='',
656 active=data[largefiles_key])
657 active=data[largefiles_key])
657 self._create_or_update_ui(
658 self._create_or_update_ui(
658 self.repo_settings, *evolve, value='',
659 self.repo_settings, *evolve, value='',
659 active=data[evolve_key])
660 active=data[evolve_key])
660 self._set_evolution(self.repo_settings, is_enabled=data[evolve_key])
661 self._set_evolution(self.repo_settings, is_enabled=data[evolve_key])
661
662
662 self._create_or_update_ui(
663 self._create_or_update_ui(
663 self.repo_settings, *phases, value=safe_str(data[phases_key]))
664 self.repo_settings, *phases, value=safe_str(data[phases_key]))
664
665
665 def create_or_update_global_hg_settings(self, data):
666 def create_or_update_global_hg_settings(self, data):
666 opts_len = 3
667 opts_len = 3
667 largefiles, phases, evolve \
668 largefiles, phases, evolve \
668 = self.GLOBAL_HG_SETTINGS[:opts_len]
669 = self.GLOBAL_HG_SETTINGS[:opts_len]
669 largefiles_key, phases_key, evolve_key \
670 largefiles_key, phases_key, evolve_key \
670 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS[:opts_len], data)
671 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS[:opts_len], data)
671
672
672 self._create_or_update_ui(
673 self._create_or_update_ui(
673 self.global_settings, *largefiles, value='',
674 self.global_settings, *largefiles, value='',
674 active=data[largefiles_key])
675 active=data[largefiles_key])
675 self._create_or_update_ui(
676 self._create_or_update_ui(
676 self.global_settings, *phases, value=safe_str(data[phases_key]))
677 self.global_settings, *phases, value=safe_str(data[phases_key]))
677 self._create_or_update_ui(
678 self._create_or_update_ui(
678 self.global_settings, *evolve, value='',
679 self.global_settings, *evolve, value='',
679 active=data[evolve_key])
680 active=data[evolve_key])
680 self._set_evolution(self.global_settings, is_enabled=data[evolve_key])
681 self._set_evolution(self.global_settings, is_enabled=data[evolve_key])
681
682
682 def create_or_update_repo_git_settings(self, data):
683 def create_or_update_repo_git_settings(self, data):
683 # NOTE(marcink): # comma makes unpack work properly
684 # NOTE(marcink): # comma makes unpack work properly
684 lfs_enabled, \
685 lfs_enabled, \
685 = self.GIT_SETTINGS
686 = self.GIT_SETTINGS
686
687
687 lfs_enabled_key, \
688 lfs_enabled_key, \
688 = self._get_settings_keys(self.GIT_SETTINGS, data)
689 = self._get_settings_keys(self.GIT_SETTINGS, data)
689
690
690 self._create_or_update_ui(
691 self._create_or_update_ui(
691 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
692 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
692 active=data[lfs_enabled_key])
693 active=data[lfs_enabled_key])
693
694
694 def create_or_update_global_git_settings(self, data):
695 def create_or_update_global_git_settings(self, data):
695 lfs_enabled = self.GLOBAL_GIT_SETTINGS[0]
696 lfs_enabled = self.GLOBAL_GIT_SETTINGS[0]
696 lfs_enabled_key = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)[0]
697 lfs_enabled_key = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)[0]
697
698
698 self._create_or_update_ui(
699 self._create_or_update_ui(
699 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
700 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
700 active=data[lfs_enabled_key])
701 active=data[lfs_enabled_key])
701
702
702 def create_or_update_global_svn_settings(self, data):
703 def create_or_update_global_svn_settings(self, data):
703 # branch/tags patterns
704 # branch/tags patterns
704 self._create_svn_settings(self.global_settings, data)
705 self._create_svn_settings(self.global_settings, data)
705
706
706 @assert_repo_settings
707 @assert_repo_settings
707 def delete_repo_svn_pattern(self, id_):
708 def delete_repo_svn_pattern(self, id_):
708 ui = self.repo_settings.UiDbModel.get(id_)
709 ui = self.repo_settings.UiDbModel.get(id_)
709 if ui and ui.repository.repo_name == self.repo_settings.repo:
710 if ui and ui.repository.repo_name == self.repo_settings.repo:
710 # only delete if it's the same repo as initialized settings
711 # only delete if it's the same repo as initialized settings
711 self.repo_settings.delete_ui(id_)
712 self.repo_settings.delete_ui(id_)
712 else:
713 else:
713 # raise error as if we wouldn't find this option
714 # raise error as if we wouldn't find this option
714 self.repo_settings.delete_ui(-1)
715 self.repo_settings.delete_ui(-1)
715
716
716 def delete_global_svn_pattern(self, id_):
717 def delete_global_svn_pattern(self, id_):
717 self.global_settings.delete_ui(id_)
718 self.global_settings.delete_ui(id_)
718
719
719 @assert_repo_settings
720 @assert_repo_settings
720 def get_repo_ui_settings(self, section=None, key=None):
721 def get_repo_ui_settings(self, section=None, key=None):
721 global_uis = self.global_settings.get_ui(section, key)
722 global_uis = self.global_settings.get_ui(section, key)
722 repo_uis = self.repo_settings.get_ui(section, key)
723 repo_uis = self.repo_settings.get_ui(section, key)
723
724
724 filtered_repo_uis = self._filter_ui_settings(repo_uis)
725 filtered_repo_uis = self._filter_ui_settings(repo_uis)
725 filtered_repo_uis_keys = [
726 filtered_repo_uis_keys = [
726 (s.section, s.key) for s in filtered_repo_uis]
727 (s.section, s.key) for s in filtered_repo_uis]
727
728
728 def _is_global_ui_filtered(ui):
729 def _is_global_ui_filtered(ui):
729 return (
730 return (
730 (ui.section, ui.key) in filtered_repo_uis_keys
731 (ui.section, ui.key) in filtered_repo_uis_keys
731 or ui.section in self._svn_sections)
732 or ui.section in self._svn_sections)
732
733
733 filtered_global_uis = [
734 filtered_global_uis = [
734 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
735 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
735
736
736 return filtered_global_uis + filtered_repo_uis
737 return filtered_global_uis + filtered_repo_uis
737
738
738 def get_global_ui_settings(self, section=None, key=None):
739 def get_global_ui_settings(self, section=None, key=None):
739 return self.global_settings.get_ui(section, key)
740 return self.global_settings.get_ui(section, key)
740
741
741 def get_ui_settings_as_config_obj(self, section=None, key=None):
742 def get_ui_settings_as_config_obj(self, section=None, key=None):
742 config = base.Config()
743 config = base.Config()
743
744
744 ui_settings = self.get_ui_settings(section=section, key=key)
745 ui_settings = self.get_ui_settings(section=section, key=key)
745
746
746 for entry in ui_settings:
747 for entry in ui_settings:
747 config.set(entry.section, entry.key, entry.value)
748 config.set(entry.section, entry.key, entry.value)
748
749
749 return config
750 return config
750
751
751 def get_ui_settings(self, section=None, key=None):
752 def get_ui_settings(self, section=None, key=None):
752 if not self.repo_settings or self.inherit_global_settings:
753 if not self.repo_settings or self.inherit_global_settings:
753 return self.get_global_ui_settings(section, key)
754 return self.get_global_ui_settings(section, key)
754 else:
755 else:
755 return self.get_repo_ui_settings(section, key)
756 return self.get_repo_ui_settings(section, key)
756
757
757 def get_svn_patterns(self, section=None):
758 def get_svn_patterns(self, section=None):
758 if not self.repo_settings:
759 if not self.repo_settings:
759 return self.get_global_ui_settings(section)
760 return self.get_global_ui_settings(section)
760 else:
761 else:
761 return self.get_repo_ui_settings(section)
762 return self.get_repo_ui_settings(section)
762
763
763 @assert_repo_settings
764 @assert_repo_settings
764 def get_repo_general_settings(self):
765 def get_repo_general_settings(self):
765 global_settings = self.global_settings.get_all_settings()
766 global_settings = self.global_settings.get_all_settings()
766 repo_settings = self.repo_settings.get_all_settings()
767 repo_settings = self.repo_settings.get_all_settings()
767 filtered_repo_settings = self._filter_general_settings(repo_settings)
768 filtered_repo_settings = self._filter_general_settings(repo_settings)
768 global_settings.update(filtered_repo_settings)
769 global_settings.update(filtered_repo_settings)
769 return global_settings
770 return global_settings
770
771
771 def get_global_general_settings(self):
772 def get_global_general_settings(self):
772 return self.global_settings.get_all_settings()
773 return self.global_settings.get_all_settings()
773
774
774 def get_general_settings(self):
775 def get_general_settings(self):
775 if not self.repo_settings or self.inherit_global_settings:
776 if not self.repo_settings or self.inherit_global_settings:
776 return self.get_global_general_settings()
777 return self.get_global_general_settings()
777 else:
778 else:
778 return self.get_repo_general_settings()
779 return self.get_repo_general_settings()
779
780
780 def _filter_ui_settings(self, settings):
781 def _filter_ui_settings(self, settings):
781 filtered_settings = [
782 filtered_settings = [
782 s for s in settings if self._should_keep_setting(s)]
783 s for s in settings if self._should_keep_setting(s)]
783 return filtered_settings
784 return filtered_settings
784
785
785 def _should_keep_setting(self, setting):
786 def _should_keep_setting(self, setting):
786 keep = (
787 keep = (
787 (setting.section, setting.key) in self._ui_settings or
788 (setting.section, setting.key) in self._ui_settings or
788 setting.section in self._svn_sections)
789 setting.section in self._svn_sections)
789 return keep
790 return keep
790
791
791 def _filter_general_settings(self, settings):
792 def _filter_general_settings(self, settings):
792 keys = [self.get_keyname(key) for key in self.GENERAL_SETTINGS]
793 keys = [self.get_keyname(key) for key in self.GENERAL_SETTINGS]
793 return {
794 return {
794 k: settings[k]
795 k: settings[k]
795 for k in settings if k in keys}
796 for k in settings if k in keys}
796
797
797 def _collect_all_settings(self, global_=False):
798 def _collect_all_settings(self, global_=False):
798 settings = self.global_settings if global_ else self.repo_settings
799 settings = self.global_settings if global_ else self.repo_settings
799 result = {}
800 result = {}
800
801
801 for section, key in self._ui_settings:
802 for section, key in self._ui_settings:
802 ui = settings.get_ui_by_section_and_key(section, key)
803 ui = settings.get_ui_by_section_and_key(section, key)
803 result_key = self._get_form_ui_key(section, key)
804 result_key = self._get_form_ui_key(section, key)
804
805
805 if ui:
806 if ui:
806 if section in ('hooks', 'extensions'):
807 if section in ('hooks', 'extensions'):
807 result[result_key] = ui.ui_active
808 result[result_key] = ui.ui_active
808 elif result_key in ['vcs_git_lfs_enabled']:
809 elif result_key in ['vcs_git_lfs_enabled']:
809 result[result_key] = ui.ui_active
810 result[result_key] = ui.ui_active
810 else:
811 else:
811 result[result_key] = ui.ui_value
812 result[result_key] = ui.ui_value
812
813
813 for name in self.GENERAL_SETTINGS:
814 for name in self.GENERAL_SETTINGS:
814 setting = settings.get_setting_by_name(name)
815 setting = settings.get_setting_by_name(name)
815 if setting:
816 if setting:
816 result_key = self.get_keyname(name)
817 result_key = self.get_keyname(name)
817 result[result_key] = setting.app_settings_value
818 result[result_key] = setting.app_settings_value
818
819
819 return result
820 return result
820
821
821 def _get_form_ui_key(self, section, key):
822 def _get_form_ui_key(self, section, key):
822 return '{section}_{key}'.format(
823 return '{section}_{key}'.format(
823 section=section, key=key.replace('.', '_'))
824 section=section, key=key.replace('.', '_'))
824
825
825 def _create_or_update_ui(
826 def _create_or_update_ui(
826 self, settings, section, key, value=None, active=None):
827 self, settings, section, key, value=None, active=None):
827 ui = settings.get_ui_by_section_and_key(section, key)
828 ui = settings.get_ui_by_section_and_key(section, key)
828 if not ui:
829 if not ui:
829 active = True if active is None else active
830 active = True if active is None else active
830 settings.create_ui_section_value(
831 settings.create_ui_section_value(
831 section, value, key=key, active=active)
832 section, value, key=key, active=active)
832 else:
833 else:
833 if active is not None:
834 if active is not None:
834 ui.ui_active = active
835 ui.ui_active = active
835 if value is not None:
836 if value is not None:
836 ui.ui_value = value
837 ui.ui_value = value
837 Session().add(ui)
838 Session().add(ui)
838
839
839 def _create_svn_settings(self, settings, data):
840 def _create_svn_settings(self, settings, data):
840 svn_settings = {
841 svn_settings = {
841 'new_svn_branch': self.SVN_BRANCH_SECTION,
842 'new_svn_branch': self.SVN_BRANCH_SECTION,
842 'new_svn_tag': self.SVN_TAG_SECTION
843 'new_svn_tag': self.SVN_TAG_SECTION
843 }
844 }
844 for key in svn_settings:
845 for key in svn_settings:
845 if data.get(key):
846 if data.get(key):
846 settings.create_ui_section_value(svn_settings[key], data[key])
847 settings.create_ui_section_value(svn_settings[key], data[key])
847
848
848 def _create_or_update_general_settings(self, settings, data):
849 def _create_or_update_general_settings(self, settings, data):
849 for name in self.GENERAL_SETTINGS:
850 for name in self.GENERAL_SETTINGS:
850 data_key = self.get_keyname(name)
851 data_key = self.get_keyname(name)
851 if data_key not in data:
852 if data_key not in data:
852 raise ValueError(
853 raise ValueError(
853 f'The given data does not contain {data_key} key')
854 f'The given data does not contain {data_key} key')
854 setting = settings.create_or_update_setting(
855 setting = settings.create_or_update_setting(
855 name, data[data_key], 'bool')
856 name, data[data_key], 'bool')
856 Session().add(setting)
857 Session().add(setting)
857
858
858 def _get_settings_keys(self, settings, data):
859 def _get_settings_keys(self, settings, data):
859 data_keys = [self._get_form_ui_key(*s) for s in settings]
860 data_keys = [self._get_form_ui_key(*s) for s in settings]
860 for data_key in data_keys:
861 for data_key in data_keys:
861 if data_key not in data:
862 if data_key not in data:
862 raise ValueError(
863 raise ValueError(
863 f'The given data does not contain {data_key} key')
864 f'The given data does not contain {data_key} key')
864 return data_keys
865 return data_keys
865
866
866 def create_largeobjects_dirs_if_needed(self, repo_store_path):
867 def create_largeobjects_dirs_if_needed(self, repo_store_path):
867 """
868 """
868 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
869 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
869 does a repository scan if enabled in the settings.
870 does a repository scan if enabled in the settings.
870 """
871 """
871
872
872 from rhodecode.lib.vcs.backends.hg import largefiles_store
873 from rhodecode.lib.vcs.backends.hg import largefiles_store
873 from rhodecode.lib.vcs.backends.git import lfs_store
874 from rhodecode.lib.vcs.backends.git import lfs_store
874
875
875 paths = [
876 paths = [
876 largefiles_store(repo_store_path),
877 largefiles_store(repo_store_path),
877 lfs_store(repo_store_path)]
878 lfs_store(repo_store_path)]
878
879
879 for path in paths:
880 for path in paths:
880 if os.path.isdir(path):
881 if os.path.isdir(path):
881 continue
882 continue
882 if os.path.isfile(path):
883 if os.path.isfile(path):
883 continue
884 continue
884 # not a file nor dir, we try to create it
885 # not a file nor dir, we try to create it
885 try:
886 try:
886 os.makedirs(path)
887 os.makedirs(path)
887 except Exception:
888 except Exception:
888 log.warning('Failed to create largefiles dir:%s', path)
889 log.warning('Failed to create largefiles dir:%s', path)
@@ -1,422 +1,450
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 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 import io
18 import io
19 import shlex
19 import shlex
20
20
21 import math
21 import math
22 import re
22 import re
23 import os
23 import os
24 import datetime
24 import datetime
25 import logging
25 import logging
26 import queue
26 import queue
27 import subprocess
27 import subprocess
28
28
29
29
30 from dateutil.parser import parse
30 from dateutil.parser import parse
31 from pyramid.interfaces import IRoutesMapper
31 from pyramid.interfaces import IRoutesMapper
32 from pyramid.settings import asbool
32 from pyramid.settings import asbool
33 from pyramid.path import AssetResolver
33 from pyramid.path import AssetResolver
34 from threading import Thread
34 from threading import Thread
35
35
36 from rhodecode.config.jsroutes import generate_jsroutes_content
36 from rhodecode.config.jsroutes import generate_jsroutes_content
37 from rhodecode.lib.base import get_auth_user
37 from rhodecode.lib.base import get_auth_user
38 from rhodecode.lib.celerylib.loader import set_celery_conf
38 from rhodecode.lib.celerylib.loader import set_celery_conf
39
39
40 import rhodecode
40 import rhodecode
41
41
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 def add_renderer_globals(event):
46 def add_renderer_globals(event):
47 from rhodecode.lib import helpers
47 from rhodecode.lib import helpers
48
48
49 # TODO: When executed in pyramid view context the request is not available
49 # TODO: When executed in pyramid view context the request is not available
50 # in the event. Find a better solution to get the request.
50 # in the event. Find a better solution to get the request.
51 from pyramid.threadlocal import get_current_request
51 from pyramid.threadlocal import get_current_request
52 request = event['request'] or get_current_request()
52 request = event['request'] or get_current_request()
53
53
54 # Add Pyramid translation as '_' to context
54 # Add Pyramid translation as '_' to context
55 event['_'] = request.translate
55 event['_'] = request.translate
56 event['_ungettext'] = request.plularize
56 event['_ungettext'] = request.plularize
57 event['h'] = helpers
57 event['h'] = helpers
58
58
59
59
60 def auto_merge_pr_if_needed(event):
61 from rhodecode.model.db import PullRequest
62 from rhodecode.model.pull_request import (
63 PullRequestModel, ChangesetStatus, MergeCheck
64 )
65
66 pr_event_data = event.as_dict()['pullrequest']
67 pull_request = PullRequest.get(pr_event_data['pull_request_id'])
68 calculated_status = pr_event_data['status']
69 if (calculated_status == ChangesetStatus.STATUS_APPROVED
70 and PullRequestModel().is_automatic_merge_enabled(pull_request)):
71 user = pull_request.author.AuthUser()
72
73 merge_check = MergeCheck.validate(
74 pull_request, user, translator=lambda x: x, fail_early=True
75 )
76 if merge_check.merge_possible:
77 from rhodecode.lib.base import vcs_operation_context
78 extras = vcs_operation_context(
79 event.request.environ, repo_name=pull_request.target_repo.repo_name,
80 username=user.username, action='push',
81 scm=pull_request.target_repo.repo_type)
82 from rc_ee.lib.celerylib.tasks import auto_merge_repo
83 auto_merge_repo.apply_async(
84 args=(pull_request.pull_request_id, extras)
85 )
86
87
60 def set_user_lang(event):
88 def set_user_lang(event):
61 request = event.request
89 request = event.request
62 cur_user = getattr(request, 'user', None)
90 cur_user = getattr(request, 'user', None)
63
91
64 if cur_user:
92 if cur_user:
65 user_lang = cur_user.get_instance().user_data.get('language')
93 user_lang = cur_user.get_instance().user_data.get('language')
66 if user_lang:
94 if user_lang:
67 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
95 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
68 event.request._LOCALE_ = user_lang
96 event.request._LOCALE_ = user_lang
69
97
70
98
71 def update_celery_conf(event):
99 def update_celery_conf(event):
72 log.debug('Setting celery config from new request')
100 log.debug('Setting celery config from new request')
73 set_celery_conf(request=event.request, registry=event.request.registry)
101 set_celery_conf(request=event.request, registry=event.request.registry)
74
102
75
103
76 def add_request_user_context(event):
104 def add_request_user_context(event):
77 """
105 """
78 Adds auth user into request context
106 Adds auth user into request context
79 """
107 """
80
108
81 request = event.request
109 request = event.request
82 # access req_id as soon as possible
110 # access req_id as soon as possible
83 req_id = request.req_id
111 req_id = request.req_id
84
112
85 if hasattr(request, 'vcs_call'):
113 if hasattr(request, 'vcs_call'):
86 # skip vcs calls
114 # skip vcs calls
87 return
115 return
88
116
89 if hasattr(request, 'rpc_method'):
117 if hasattr(request, 'rpc_method'):
90 # skip api calls
118 # skip api calls
91 return
119 return
92
120
93 auth_user, auth_token = get_auth_user(request)
121 auth_user, auth_token = get_auth_user(request)
94 request.user = auth_user
122 request.user = auth_user
95 request.user_auth_token = auth_token
123 request.user_auth_token = auth_token
96 request.environ['rc_auth_user'] = auth_user
124 request.environ['rc_auth_user'] = auth_user
97 request.environ['rc_auth_user_id'] = str(auth_user.user_id)
125 request.environ['rc_auth_user_id'] = str(auth_user.user_id)
98 request.environ['rc_req_id'] = req_id
126 request.environ['rc_req_id'] = req_id
99
127
100
128
101 def reset_log_bucket(event):
129 def reset_log_bucket(event):
102 """
130 """
103 reset the log bucket on new request
131 reset the log bucket on new request
104 """
132 """
105 request = event.request
133 request = event.request
106 request.req_id_records_init()
134 request.req_id_records_init()
107
135
108
136
109 def scan_repositories_if_enabled(event):
137 def scan_repositories_if_enabled(event):
110 """
138 """
111 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
139 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
112 does a repository scan if enabled in the settings.
140 does a repository scan if enabled in the settings.
113 """
141 """
114
142
115 settings = event.app.registry.settings
143 settings = event.app.registry.settings
116 vcs_server_enabled = settings['vcs.server.enable']
144 vcs_server_enabled = settings['vcs.server.enable']
117 import_on_startup = settings['startup.import_repos']
145 import_on_startup = settings['startup.import_repos']
118
146
119 if vcs_server_enabled and import_on_startup:
147 if vcs_server_enabled and import_on_startup:
120 from rhodecode.model.scm import ScmModel
148 from rhodecode.model.scm import ScmModel
121 from rhodecode.lib.utils import repo2db_mapper
149 from rhodecode.lib.utils import repo2db_mapper
122 scm = ScmModel()
150 scm = ScmModel()
123 repositories = scm.repo_scan(scm.repos_path)
151 repositories = scm.repo_scan(scm.repos_path)
124 repo2db_mapper(repositories, remove_obsolete=False)
152 repo2db_mapper(repositories, remove_obsolete=False)
125
153
126
154
127 def write_metadata_if_needed(event):
155 def write_metadata_if_needed(event):
128 """
156 """
129 Writes upgrade metadata
157 Writes upgrade metadata
130 """
158 """
131 import rhodecode
159 import rhodecode
132 from rhodecode.lib import system_info
160 from rhodecode.lib import system_info
133 from rhodecode.lib import ext_json
161 from rhodecode.lib import ext_json
134
162
135 fname = '.rcmetadata.json'
163 fname = '.rcmetadata.json'
136 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
164 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
137 metadata_destination = os.path.join(ini_loc, fname)
165 metadata_destination = os.path.join(ini_loc, fname)
138
166
139 def get_update_age():
167 def get_update_age():
140 now = datetime.datetime.utcnow()
168 now = datetime.datetime.utcnow()
141
169
142 with open(metadata_destination, 'rb') as f:
170 with open(metadata_destination, 'rb') as f:
143 data = ext_json.json.loads(f.read())
171 data = ext_json.json.loads(f.read())
144 if 'created_on' in data:
172 if 'created_on' in data:
145 update_date = parse(data['created_on'])
173 update_date = parse(data['created_on'])
146 diff = now - update_date
174 diff = now - update_date
147 return diff.total_seconds() / 60.0
175 return diff.total_seconds() / 60.0
148
176
149 return 0
177 return 0
150
178
151 def write():
179 def write():
152 configuration = system_info.SysInfo(
180 configuration = system_info.SysInfo(
153 system_info.rhodecode_config)()['value']
181 system_info.rhodecode_config)()['value']
154 license_token = configuration['config']['license_token']
182 license_token = configuration['config']['license_token']
155
183
156 setup = dict(
184 setup = dict(
157 workers=configuration['config']['server:main'].get(
185 workers=configuration['config']['server:main'].get(
158 'workers', '?'),
186 'workers', '?'),
159 worker_type=configuration['config']['server:main'].get(
187 worker_type=configuration['config']['server:main'].get(
160 'worker_class', 'sync'),
188 'worker_class', 'sync'),
161 )
189 )
162 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
190 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
163 del dbinfo['url']
191 del dbinfo['url']
164
192
165 metadata = dict(
193 metadata = dict(
166 desc='upgrade metadata info',
194 desc='upgrade metadata info',
167 license_token=license_token,
195 license_token=license_token,
168 created_on=datetime.datetime.utcnow().isoformat(),
196 created_on=datetime.datetime.utcnow().isoformat(),
169 usage=system_info.SysInfo(system_info.usage_info)()['value'],
197 usage=system_info.SysInfo(system_info.usage_info)()['value'],
170 platform=system_info.SysInfo(system_info.platform_type)()['value'],
198 platform=system_info.SysInfo(system_info.platform_type)()['value'],
171 database=dbinfo,
199 database=dbinfo,
172 cpu=system_info.SysInfo(system_info.cpu)()['value'],
200 cpu=system_info.SysInfo(system_info.cpu)()['value'],
173 memory=system_info.SysInfo(system_info.memory)()['value'],
201 memory=system_info.SysInfo(system_info.memory)()['value'],
174 setup=setup
202 setup=setup
175 )
203 )
176
204
177 with open(metadata_destination, 'wb') as f:
205 with open(metadata_destination, 'wb') as f:
178 f.write(ext_json.json.dumps(metadata))
206 f.write(ext_json.json.dumps(metadata))
179
207
180 settings = event.app.registry.settings
208 settings = event.app.registry.settings
181 if settings.get('metadata.skip'):
209 if settings.get('metadata.skip'):
182 return
210 return
183
211
184 # only write this every 24h, workers restart caused unwanted delays
212 # only write this every 24h, workers restart caused unwanted delays
185 try:
213 try:
186 age_in_min = get_update_age()
214 age_in_min = get_update_age()
187 except Exception:
215 except Exception:
188 age_in_min = 0
216 age_in_min = 0
189
217
190 if age_in_min > 60 * 60 * 24:
218 if age_in_min > 60 * 60 * 24:
191 return
219 return
192
220
193 try:
221 try:
194 write()
222 write()
195 except Exception:
223 except Exception:
196 pass
224 pass
197
225
198
226
199 def write_usage_data(event):
227 def write_usage_data(event):
200 import rhodecode
228 import rhodecode
201 from rhodecode.lib import system_info
229 from rhodecode.lib import system_info
202 from rhodecode.lib import ext_json
230 from rhodecode.lib import ext_json
203
231
204 settings = event.app.registry.settings
232 settings = event.app.registry.settings
205 instance_tag = settings.get('metadata.write_usage_tag')
233 instance_tag = settings.get('metadata.write_usage_tag')
206 if not settings.get('metadata.write_usage'):
234 if not settings.get('metadata.write_usage'):
207 return
235 return
208
236
209 def get_update_age(dest_file):
237 def get_update_age(dest_file):
210 now = datetime.datetime.now(datetime.UTC)
238 now = datetime.datetime.now(datetime.UTC)
211
239
212 with open(dest_file, 'rb') as f:
240 with open(dest_file, 'rb') as f:
213 data = ext_json.json.loads(f.read())
241 data = ext_json.json.loads(f.read())
214 if 'created_on' in data:
242 if 'created_on' in data:
215 update_date = parse(data['created_on'])
243 update_date = parse(data['created_on'])
216 diff = now - update_date
244 diff = now - update_date
217 return math.ceil(diff.total_seconds() / 60.0)
245 return math.ceil(diff.total_seconds() / 60.0)
218
246
219 return 0
247 return 0
220
248
221 utc_date = datetime.datetime.now(datetime.UTC)
249 utc_date = datetime.datetime.now(datetime.UTC)
222 hour_quarter = int(math.ceil((utc_date.hour + utc_date.minute/60.0) / 6.))
250 hour_quarter = int(math.ceil((utc_date.hour + utc_date.minute/60.0) / 6.))
223 fname = f'.rc_usage_{utc_date.year}{utc_date.month:02d}{utc_date.day:02d}_{hour_quarter}.json'
251 fname = f'.rc_usage_{utc_date.year}{utc_date.month:02d}{utc_date.day:02d}_{hour_quarter}.json'
224 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
252 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
225
253
226 usage_dir = os.path.join(ini_loc, '.rcusage')
254 usage_dir = os.path.join(ini_loc, '.rcusage')
227 if not os.path.isdir(usage_dir):
255 if not os.path.isdir(usage_dir):
228 os.makedirs(usage_dir)
256 os.makedirs(usage_dir)
229 usage_metadata_destination = os.path.join(usage_dir, fname)
257 usage_metadata_destination = os.path.join(usage_dir, fname)
230
258
231 try:
259 try:
232 age_in_min = get_update_age(usage_metadata_destination)
260 age_in_min = get_update_age(usage_metadata_destination)
233 except Exception:
261 except Exception:
234 age_in_min = 0
262 age_in_min = 0
235
263
236 # write every 6th hour
264 # write every 6th hour
237 if age_in_min and age_in_min < 60 * 6:
265 if age_in_min and age_in_min < 60 * 6:
238 log.debug('Usage file created %s minutes ago, skipping (threshold: %s minutes)...',
266 log.debug('Usage file created %s minutes ago, skipping (threshold: %s minutes)...',
239 age_in_min, 60 * 6)
267 age_in_min, 60 * 6)
240 return
268 return
241
269
242 def write(dest_file):
270 def write(dest_file):
243 configuration = system_info.SysInfo(system_info.rhodecode_config)()['value']
271 configuration = system_info.SysInfo(system_info.rhodecode_config)()['value']
244 license_token = configuration['config']['license_token']
272 license_token = configuration['config']['license_token']
245
273
246 metadata = dict(
274 metadata = dict(
247 desc='Usage data',
275 desc='Usage data',
248 instance_tag=instance_tag,
276 instance_tag=instance_tag,
249 license_token=license_token,
277 license_token=license_token,
250 created_on=datetime.datetime.utcnow().isoformat(),
278 created_on=datetime.datetime.utcnow().isoformat(),
251 usage=system_info.SysInfo(system_info.usage_info)()['value'],
279 usage=system_info.SysInfo(system_info.usage_info)()['value'],
252 )
280 )
253
281
254 with open(dest_file, 'wb') as f:
282 with open(dest_file, 'wb') as f:
255 f.write(ext_json.formatted_json(metadata))
283 f.write(ext_json.formatted_json(metadata))
256
284
257 try:
285 try:
258 log.debug('Writing usage file at: %s', usage_metadata_destination)
286 log.debug('Writing usage file at: %s', usage_metadata_destination)
259 write(usage_metadata_destination)
287 write(usage_metadata_destination)
260 except Exception:
288 except Exception:
261 pass
289 pass
262
290
263
291
264 def write_js_routes_if_enabled(event):
292 def write_js_routes_if_enabled(event):
265 registry = event.app.registry
293 registry = event.app.registry
266
294
267 mapper = registry.queryUtility(IRoutesMapper)
295 mapper = registry.queryUtility(IRoutesMapper)
268 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
296 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
269
297
270 def _extract_route_information(route):
298 def _extract_route_information(route):
271 """
299 """
272 Convert a route into tuple(name, path, args), eg:
300 Convert a route into tuple(name, path, args), eg:
273 ('show_user', '/profile/%(username)s', ['username'])
301 ('show_user', '/profile/%(username)s', ['username'])
274 """
302 """
275
303
276 route_path = route.pattern
304 route_path = route.pattern
277 pattern = route.pattern
305 pattern = route.pattern
278
306
279 def replace(matchobj):
307 def replace(matchobj):
280 if matchobj.group(1):
308 if matchobj.group(1):
281 return "%%(%s)s" % matchobj.group(1).split(':')[0]
309 return "%%(%s)s" % matchobj.group(1).split(':')[0]
282 else:
310 else:
283 return "%%(%s)s" % matchobj.group(2)
311 return "%%(%s)s" % matchobj.group(2)
284
312
285 route_path = _argument_prog.sub(replace, route_path)
313 route_path = _argument_prog.sub(replace, route_path)
286
314
287 if not route_path.startswith('/'):
315 if not route_path.startswith('/'):
288 route_path = f'/{route_path}'
316 route_path = f'/{route_path}'
289
317
290 return (
318 return (
291 route.name,
319 route.name,
292 route_path,
320 route_path,
293 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
321 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
294 for arg in _argument_prog.findall(pattern)]
322 for arg in _argument_prog.findall(pattern)]
295 )
323 )
296
324
297 def get_routes():
325 def get_routes():
298 # pyramid routes
326 # pyramid routes
299 for route in mapper.get_routes():
327 for route in mapper.get_routes():
300 if not route.name.startswith('__'):
328 if not route.name.startswith('__'):
301 yield _extract_route_information(route)
329 yield _extract_route_information(route)
302
330
303 if asbool(registry.settings.get('generate_js_files', 'false')):
331 if asbool(registry.settings.get('generate_js_files', 'false')):
304 static_path = AssetResolver().resolve('rhodecode:public').abspath()
332 static_path = AssetResolver().resolve('rhodecode:public').abspath()
305 jsroutes = get_routes()
333 jsroutes = get_routes()
306 jsroutes_file_content = generate_jsroutes_content(jsroutes)
334 jsroutes_file_content = generate_jsroutes_content(jsroutes)
307 jsroutes_file_path = os.path.join(
335 jsroutes_file_path = os.path.join(
308 static_path, 'js', 'rhodecode', 'routes.js')
336 static_path, 'js', 'rhodecode', 'routes.js')
309
337
310 try:
338 try:
311 with open(jsroutes_file_path, 'w', encoding='utf-8') as f:
339 with open(jsroutes_file_path, 'w', encoding='utf-8') as f:
312 f.write(jsroutes_file_content)
340 f.write(jsroutes_file_content)
313 log.debug('generated JS files in %s', jsroutes_file_path)
341 log.debug('generated JS files in %s', jsroutes_file_path)
314 except Exception:
342 except Exception:
315 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
343 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
316
344
317
345
318 def import_license_if_present(event):
346 def import_license_if_present(event):
319 """
347 """
320 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
348 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
321 does a import license key based on a presence of the file.
349 does a import license key based on a presence of the file.
322 """
350 """
323 settings = event.app.registry.settings
351 settings = event.app.registry.settings
324
352
325 rhodecode_edition_id = settings.get('rhodecode.edition_id')
353 rhodecode_edition_id = settings.get('rhodecode.edition_id')
326 license_file_path = settings.get('license.import_path')
354 license_file_path = settings.get('license.import_path')
327 force = settings.get('license.import_path_mode') == 'force'
355 force = settings.get('license.import_path_mode') == 'force'
328
356
329 if license_file_path and rhodecode_edition_id == 'EE':
357 if license_file_path and rhodecode_edition_id == 'EE':
330 log.debug('license.import_path= is set importing license from %s', license_file_path)
358 log.debug('license.import_path= is set importing license from %s', license_file_path)
331 from rhodecode.model.meta import Session
359 from rhodecode.model.meta import Session
332 from rhodecode.model.license import apply_license_from_file
360 from rhodecode.model.license import apply_license_from_file
333 try:
361 try:
334 apply_license_from_file(license_file_path, force=force)
362 apply_license_from_file(license_file_path, force=force)
335 Session().commit()
363 Session().commit()
336 except OSError:
364 except OSError:
337 log.exception('Failed to import license from %s, make sure this file exists', license_file_path)
365 log.exception('Failed to import license from %s, make sure this file exists', license_file_path)
338
366
339
367
340 class Subscriber(object):
368 class Subscriber(object):
341 """
369 """
342 Base class for subscribers to the pyramid event system.
370 Base class for subscribers to the pyramid event system.
343 """
371 """
344 def __call__(self, event):
372 def __call__(self, event):
345 self.run(event)
373 self.run(event)
346
374
347 def run(self, event):
375 def run(self, event):
348 raise NotImplementedError('Subclass has to implement this.')
376 raise NotImplementedError('Subclass has to implement this.')
349
377
350
378
351 class AsyncSubscriber(Subscriber):
379 class AsyncSubscriber(Subscriber):
352 """
380 """
353 Subscriber that handles the execution of events in a separate task to not
381 Subscriber that handles the execution of events in a separate task to not
354 block the execution of the code which triggers the event. It puts the
382 block the execution of the code which triggers the event. It puts the
355 received events into a queue from which the worker process takes them in
383 received events into a queue from which the worker process takes them in
356 order.
384 order.
357 """
385 """
358 def __init__(self):
386 def __init__(self):
359 self._stop = False
387 self._stop = False
360 self._eventq = queue.Queue()
388 self._eventq = queue.Queue()
361 self._worker = self.create_worker()
389 self._worker = self.create_worker()
362 self._worker.start()
390 self._worker.start()
363
391
364 def __call__(self, event):
392 def __call__(self, event):
365 self._eventq.put(event)
393 self._eventq.put(event)
366
394
367 def create_worker(self):
395 def create_worker(self):
368 worker = Thread(target=self.do_work)
396 worker = Thread(target=self.do_work)
369 worker.daemon = True
397 worker.daemon = True
370 return worker
398 return worker
371
399
372 def stop_worker(self):
400 def stop_worker(self):
373 self._stop = False
401 self._stop = False
374 self._eventq.put(None)
402 self._eventq.put(None)
375 self._worker.join()
403 self._worker.join()
376
404
377 def do_work(self):
405 def do_work(self):
378 while not self._stop:
406 while not self._stop:
379 event = self._eventq.get()
407 event = self._eventq.get()
380 if event is not None:
408 if event is not None:
381 self.run(event)
409 self.run(event)
382
410
383
411
384 class AsyncSubprocessSubscriber(AsyncSubscriber):
412 class AsyncSubprocessSubscriber(AsyncSubscriber):
385 """
413 """
386 Subscriber that uses the subprocess module to execute a command if an
414 Subscriber that uses the subprocess module to execute a command if an
387 event is received. Events are handled asynchronously::
415 event is received. Events are handled asynchronously::
388
416
389 subscriber = AsyncSubprocessSubscriber('ls -la', timeout=10)
417 subscriber = AsyncSubprocessSubscriber('ls -la', timeout=10)
390 subscriber(dummyEvent) # running __call__(event)
418 subscriber(dummyEvent) # running __call__(event)
391
419
392 """
420 """
393
421
394 def __init__(self, cmd, timeout=None):
422 def __init__(self, cmd, timeout=None):
395 if not isinstance(cmd, (list, tuple)):
423 if not isinstance(cmd, (list, tuple)):
396 cmd = shlex.split(cmd)
424 cmd = shlex.split(cmd)
397 super().__init__()
425 super().__init__()
398 self._cmd = cmd
426 self._cmd = cmd
399 self._timeout = timeout
427 self._timeout = timeout
400
428
401 def run(self, event):
429 def run(self, event):
402 cmd = self._cmd
430 cmd = self._cmd
403 timeout = self._timeout
431 timeout = self._timeout
404 log.debug('Executing command %s.', cmd)
432 log.debug('Executing command %s.', cmd)
405
433
406 try:
434 try:
407 output = subprocess.check_output(
435 output = subprocess.check_output(
408 cmd, timeout=timeout, stderr=subprocess.STDOUT)
436 cmd, timeout=timeout, stderr=subprocess.STDOUT)
409 log.debug('Command finished %s', cmd)
437 log.debug('Command finished %s', cmd)
410 if output:
438 if output:
411 log.debug('Command output: %s', output)
439 log.debug('Command output: %s', output)
412 except subprocess.TimeoutExpired as e:
440 except subprocess.TimeoutExpired as e:
413 log.exception('Timeout while executing command.')
441 log.exception('Timeout while executing command.')
414 if e.output:
442 if e.output:
415 log.error('Command output: %s', e.output)
443 log.error('Command output: %s', e.output)
416 except subprocess.CalledProcessError as e:
444 except subprocess.CalledProcessError as e:
417 log.exception('Error while executing command.')
445 log.exception('Error while executing command.')
418 if e.output:
446 if e.output:
419 log.error('Command output: %s', e.output)
447 log.error('Command output: %s', e.output)
420 except Exception:
448 except Exception:
421 log.exception(
449 log.exception(
422 'Exception while executing command %s.', cmd)
450 'Exception while executing command %s.', cmd)
@@ -1,308 +1,325
1 ## snippet for displaying vcs settings
1 ## snippet for displaying vcs settings
2 ## usage:
2 ## usage:
3 ## <%namespace name="vcss" file="/base/vcssettings.mako"/>
3 ## <%namespace name="vcss" file="/base/vcssettings.mako"/>
4 ## ${vcss.vcs_settings_fields()}
4 ## ${vcss.vcs_settings_fields()}
5
5
6 <%def name="vcs_settings_fields(suffix='', svn_branch_patterns=None, svn_tag_patterns=None, repo_type=None, display_globals=False, **kwargs)">
6 <%def name="vcs_settings_fields(suffix='', svn_branch_patterns=None, svn_tag_patterns=None, repo_type=None, display_globals=False, **kwargs)">
7 % if display_globals:
7 % if display_globals:
8
8
9 % endif
9 % endif
10
10
11 % if display_globals or repo_type in ['git', 'hg']:
11 % if display_globals or repo_type in ['git', 'hg']:
12 <div class="panel panel-default">
12 <div class="panel panel-default">
13 <div class="panel-heading" id="vcs-hooks-options">
13 <div class="panel-heading" id="vcs-hooks-options">
14 <h3 class="panel-title">${_('Internal Hooks')}<a class="permalink" href="#vcs-hooks-options"> ΒΆ</a></h3>
14 <h3 class="panel-title">${_('Internal Hooks')}<a class="permalink" href="#vcs-hooks-options"> ΒΆ</a></h3>
15 </div>
15 </div>
16 <div class="panel-body">
16 <div class="panel-body">
17 <div class="field">
17 <div class="field">
18 <div class="checkbox">
18 <div class="checkbox">
19 ${h.checkbox('hooks_changegroup_repo_size' + suffix, 'True', **kwargs)}
19 ${h.checkbox('hooks_changegroup_repo_size' + suffix, 'True', **kwargs)}
20 <label for="hooks_changegroup_repo_size${suffix}">${_('Show repository size after push')}</label>
20 <label for="hooks_changegroup_repo_size${suffix}">${_('Show repository size after push')}</label>
21 </div>
21 </div>
22
22
23 <div class="label">
23 <div class="label">
24 <span class="help-block">${_('Trigger a hook that calculates repository size after each push.')}</span>
24 <span class="help-block">${_('Trigger a hook that calculates repository size after each push.')}</span>
25 </div>
25 </div>
26 <div class="checkbox">
26 <div class="checkbox">
27 ${h.checkbox('hooks_changegroup_push_logger' + suffix, 'True', **kwargs)}
27 ${h.checkbox('hooks_changegroup_push_logger' + suffix, 'True', **kwargs)}
28 <label for="hooks_changegroup_push_logger${suffix}">${_('Execute pre/post push hooks')}</label>
28 <label for="hooks_changegroup_push_logger${suffix}">${_('Execute pre/post push hooks')}</label>
29 </div>
29 </div>
30 <div class="label">
30 <div class="label">
31 <span class="help-block">${_('Execute Built in pre/post push hooks. This also executes rcextensions hooks.')}</span>
31 <span class="help-block">${_('Execute Built in pre/post push hooks. This also executes rcextensions hooks.')}</span>
32 </div>
32 </div>
33 <div class="checkbox">
33 <div class="checkbox">
34 ${h.checkbox('hooks_outgoing_pull_logger' + suffix, 'True', **kwargs)}
34 ${h.checkbox('hooks_outgoing_pull_logger' + suffix, 'True', **kwargs)}
35 <label for="hooks_outgoing_pull_logger${suffix}">${_('Execute pre/post pull hooks')}</label>
35 <label for="hooks_outgoing_pull_logger${suffix}">${_('Execute pre/post pull hooks')}</label>
36 </div>
36 </div>
37 <div class="label">
37 <div class="label">
38 <span class="help-block">${_('Execute Built in pre/post pull hooks. This also executes rcextensions hooks.')}</span>
38 <span class="help-block">${_('Execute Built in pre/post pull hooks. This also executes rcextensions hooks.')}</span>
39 </div>
39 </div>
40 </div>
40 </div>
41 </div>
41 </div>
42 </div>
42 </div>
43 % endif
43 % endif
44
44
45 % if display_globals or repo_type in ['hg']:
45 % if display_globals or repo_type in ['hg']:
46 <div class="panel panel-default">
46 <div class="panel panel-default">
47 <div class="panel-heading" id="vcs-hg-options">
47 <div class="panel-heading" id="vcs-hg-options">
48 <h3 class="panel-title">${_('Mercurial Settings')}<a class="permalink" href="#vcs-hg-options"> ΒΆ</a></h3>
48 <h3 class="panel-title">${_('Mercurial Settings')}<a class="permalink" href="#vcs-hg-options"> ΒΆ</a></h3>
49 </div>
49 </div>
50 <div class="panel-body">
50 <div class="panel-body">
51 <div class="checkbox">
51 <div class="checkbox">
52 ${h.checkbox('extensions_largefiles' + suffix, 'True', **kwargs)}
52 ${h.checkbox('extensions_largefiles' + suffix, 'True', **kwargs)}
53 <label for="extensions_largefiles${suffix}">${_('Enable largefiles extension')}</label>
53 <label for="extensions_largefiles${suffix}">${_('Enable largefiles extension')}</label>
54 </div>
54 </div>
55 <div class="label">
55 <div class="label">
56 % if display_globals:
56 % if display_globals:
57 <span class="help-block">${_('Enable Largefiles extensions for all repositories.')}</span>
57 <span class="help-block">${_('Enable Largefiles extensions for all repositories.')}</span>
58 % else:
58 % else:
59 <span class="help-block">${_('Enable Largefiles extensions for this repository.')}</span>
59 <span class="help-block">${_('Enable Largefiles extensions for this repository.')}</span>
60 % endif
60 % endif
61 </div>
61 </div>
62
62
63 <div class="checkbox">
63 <div class="checkbox">
64 ${h.checkbox('phases_publish' + suffix, 'True', **kwargs)}
64 ${h.checkbox('phases_publish' + suffix, 'True', **kwargs)}
65 <label for="phases_publish${suffix}">${_('Set repositories as publishing') if display_globals else _('Set repository as publishing')}</label>
65 <label for="phases_publish${suffix}">${_('Set repositories as publishing') if display_globals else _('Set repository as publishing')}</label>
66 </div>
66 </div>
67 <div class="label">
67 <div class="label">
68 <span class="help-block">${_('When this is enabled all commits in the repository are seen as public commits by clients.')}</span>
68 <span class="help-block">${_('When this is enabled all commits in the repository are seen as public commits by clients.')}</span>
69 </div>
69 </div>
70
70
71 <div class="checkbox">
71 <div class="checkbox">
72 ${h.checkbox('extensions_evolve' + suffix, 'True', **kwargs)}
72 ${h.checkbox('extensions_evolve' + suffix, 'True', **kwargs)}
73 <label for="extensions_evolve${suffix}">${_('Enable Evolve and Topic extension')}</label>
73 <label for="extensions_evolve${suffix}">${_('Enable Evolve and Topic extension')}</label>
74 </div>
74 </div>
75 <div class="label">
75 <div class="label">
76 % if display_globals:
76 % if display_globals:
77 <span class="help-block">${_('Enable Evolve and Topic extensions for all repositories.')}</span>
77 <span class="help-block">${_('Enable Evolve and Topic extensions for all repositories.')}</span>
78 % else:
78 % else:
79 <span class="help-block">${_('Enable Evolve and Topic extensions for this repository.')}</span>
79 <span class="help-block">${_('Enable Evolve and Topic extensions for this repository.')}</span>
80 % endif
80 % endif
81 </div>
81 </div>
82
82
83 </div>
83 </div>
84 </div>
84 </div>
85 % endif
85 % endif
86
86
87 % if display_globals or repo_type in ['git']:
87 % if display_globals or repo_type in ['git']:
88 <div class="panel panel-default">
88 <div class="panel panel-default">
89 <div class="panel-heading" id="vcs-git-options">
89 <div class="panel-heading" id="vcs-git-options">
90 <h3 class="panel-title">${_('Git Settings')}<a class="permalink" href="#vcs-git-options"> ΒΆ</a></h3>
90 <h3 class="panel-title">${_('Git Settings')}<a class="permalink" href="#vcs-git-options"> ΒΆ</a></h3>
91 </div>
91 </div>
92 <div class="panel-body">
92 <div class="panel-body">
93 <div class="checkbox">
93 <div class="checkbox">
94 ${h.checkbox('vcs_git_lfs_enabled' + suffix, 'True', **kwargs)}
94 ${h.checkbox('vcs_git_lfs_enabled' + suffix, 'True', **kwargs)}
95 <label for="vcs_git_lfs_enabled${suffix}">${_('Enable lfs extension')}</label>
95 <label for="vcs_git_lfs_enabled${suffix}">${_('Enable lfs extension')}</label>
96 </div>
96 </div>
97 <div class="label">
97 <div class="label">
98 % if display_globals:
98 % if display_globals:
99 <span class="help-block">${_('Enable lfs extensions for all repositories.')}</span>
99 <span class="help-block">${_('Enable lfs extensions for all repositories.')}</span>
100 % else:
100 % else:
101 <span class="help-block">${_('Enable lfs extensions for this repository.')}</span>
101 <span class="help-block">${_('Enable lfs extensions for this repository.')}</span>
102 % endif
102 % endif
103 </div>
103 </div>
104 </div>
104 </div>
105 </div>
105 </div>
106 % endif
106 % endif
107
107
108 % if display_globals or repo_type in ['svn']:
108 % if display_globals or repo_type in ['svn']:
109 <div class="panel panel-default">
109 <div class="panel panel-default">
110 <div class="panel-heading" id="vcs-svn-options">
110 <div class="panel-heading" id="vcs-svn-options">
111 <h3 class="panel-title">${_('Subversion Settings')}<a class="permalink" href="#vcs-svn-options"> ΒΆ</a></h3>
111 <h3 class="panel-title">${_('Subversion Settings')}<a class="permalink" href="#vcs-svn-options"> ΒΆ</a></h3>
112 </div>
112 </div>
113 <div class="panel-body">
113 <div class="panel-body">
114 % if display_globals:
114 % if display_globals:
115 <div class="field">
115 <div class="field">
116 <div class="content" >
116 <div class="content" >
117 <label>${_('mod_dav config')}</label><br/>
117 <label>${_('mod_dav config')}</label><br/>
118 <code>path: ${c.svn_config_path}</code>
118 <code>path: ${c.svn_config_path}</code>
119 </div>
119 </div>
120 <br/>
120 <br/>
121
121
122 <div>
122 <div>
123
123
124 % if c.svn_generate_config:
124 % if c.svn_generate_config:
125 <span class="buttons">
125 <span class="buttons">
126 <button class="btn btn-primary" id="vcs_svn_generate_cfg">${_('Re-generate Apache Config')}</button>
126 <button class="btn btn-primary" id="vcs_svn_generate_cfg">${_('Re-generate Apache Config')}</button>
127 </span>
127 </span>
128 % endif
128 % endif
129 </div>
129 </div>
130 </div>
130 </div>
131 % endif
131 % endif
132
132
133 <div class="field">
133 <div class="field">
134 <div class="content" >
134 <div class="content" >
135 <label>${_('Repository patterns')}</label><br/>
135 <label>${_('Repository patterns')}</label><br/>
136 </div>
136 </div>
137 </div>
137 </div>
138 <div class="label">
138 <div class="label">
139 <span class="help-block">${_('Patterns for identifying SVN branches and tags. For recursive search, use "*". Eg.: "/branches/*"')}</span>
139 <span class="help-block">${_('Patterns for identifying SVN branches and tags. For recursive search, use "*". Eg.: "/branches/*"')}</span>
140 </div>
140 </div>
141
141
142 <div class="field branch_patterns">
142 <div class="field branch_patterns">
143 <div class="input" >
143 <div class="input" >
144 <label>${_('Branches')}:</label><br/>
144 <label>${_('Branches')}:</label><br/>
145 </div>
145 </div>
146 % if svn_branch_patterns:
146 % if svn_branch_patterns:
147 % for branch in svn_branch_patterns:
147 % for branch in svn_branch_patterns:
148 <div class="input adjacent" id="${'id%s' % branch.ui_id}">
148 <div class="input adjacent" id="${'id%s' % branch.ui_id}">
149 ${h.hidden('branch_ui_key' + suffix, branch.ui_key)}
149 ${h.hidden('branch_ui_key' + suffix, branch.ui_key)}
150 ${h.text('branch_value_%d' % branch.ui_id + suffix, branch.ui_value, size=59, readonly="readonly", class_='disabled')}
150 ${h.text('branch_value_%d' % branch.ui_id + suffix, branch.ui_value, size=59, readonly="readonly", class_='disabled')}
151 % if kwargs.get('disabled') != 'disabled':
151 % if kwargs.get('disabled') != 'disabled':
152 <span class="btn btn-x" onclick="ajaxDeletePattern(${branch.ui_id},'${'id%s' % branch.ui_id}')">
152 <span class="btn btn-x" onclick="ajaxDeletePattern(${branch.ui_id},'${'id%s' % branch.ui_id}')">
153 ${_('Delete')}
153 ${_('Delete')}
154 </span>
154 </span>
155 % endif
155 % endif
156 </div>
156 </div>
157 % endfor
157 % endfor
158 %endif
158 %endif
159 </div>
159 </div>
160 % if kwargs.get('disabled') != 'disabled':
160 % if kwargs.get('disabled') != 'disabled':
161 <div class="field branch_patterns">
161 <div class="field branch_patterns">
162 <div class="input" >
162 <div class="input" >
163 ${h.text('new_svn_branch',size=59,placeholder='New branch pattern')}
163 ${h.text('new_svn_branch',size=59,placeholder='New branch pattern')}
164 </div>
164 </div>
165 </div>
165 </div>
166 % endif
166 % endif
167 <div class="field tag_patterns">
167 <div class="field tag_patterns">
168 <div class="input" >
168 <div class="input" >
169 <label>${_('Tags')}:</label><br/>
169 <label>${_('Tags')}:</label><br/>
170 </div>
170 </div>
171 % if svn_tag_patterns:
171 % if svn_tag_patterns:
172 % for tag in svn_tag_patterns:
172 % for tag in svn_tag_patterns:
173 <div class="input" id="${'id%s' % tag.ui_id + suffix}">
173 <div class="input" id="${'id%s' % tag.ui_id + suffix}">
174 ${h.hidden('tag_ui_key' + suffix, tag.ui_key)}
174 ${h.hidden('tag_ui_key' + suffix, tag.ui_key)}
175 ${h.text('tag_ui_value_new_%d' % tag.ui_id + suffix, tag.ui_value, size=59, readonly="readonly", class_='disabled tag_input')}
175 ${h.text('tag_ui_value_new_%d' % tag.ui_id + suffix, tag.ui_value, size=59, readonly="readonly", class_='disabled tag_input')}
176 % if kwargs.get('disabled') != 'disabled':
176 % if kwargs.get('disabled') != 'disabled':
177 <span class="btn btn-x" onclick="ajaxDeletePattern(${tag.ui_id},'${'id%s' % tag.ui_id}')">
177 <span class="btn btn-x" onclick="ajaxDeletePattern(${tag.ui_id},'${'id%s' % tag.ui_id}')">
178 ${_('Delete')}
178 ${_('Delete')}
179 </span>
179 </span>
180 %endif
180 %endif
181 </div>
181 </div>
182 % endfor
182 % endfor
183 % endif
183 % endif
184 </div>
184 </div>
185 % if kwargs.get('disabled') != 'disabled':
185 % if kwargs.get('disabled') != 'disabled':
186 <div class="field tag_patterns">
186 <div class="field tag_patterns">
187 <div class="input" >
187 <div class="input" >
188 ${h.text('new_svn_tag' + suffix, size=59, placeholder='New tag pattern')}
188 ${h.text('new_svn_tag' + suffix, size=59, placeholder='New tag pattern')}
189 </div>
189 </div>
190 </div>
190 </div>
191 %endif
191 %endif
192 </div>
192 </div>
193 </div>
193 </div>
194 % else:
194 % else:
195 ${h.hidden('new_svn_branch' + suffix, '')}
195 ${h.hidden('new_svn_branch' + suffix, '')}
196 ${h.hidden('new_svn_tag' + suffix, '')}
196 ${h.hidden('new_svn_tag' + suffix, '')}
197 % endif
197 % endif
198
198
199
199
200 % if display_globals or repo_type in ['hg', 'git']:
200 % if display_globals or repo_type in ['hg', 'git']:
201 <div class="panel panel-default">
201 <div class="panel panel-default">
202 <div class="panel-heading" id="vcs-pull-requests-options">
202 <div class="panel-heading" id="vcs-pull-requests-options">
203 <h3 class="panel-title">${_('Pull Request Settings')}<a class="permalink" href="#vcs-pull-requests-options"> ΒΆ</a></h3>
203 <h3 class="panel-title">${_('Pull Request Settings')}<a class="permalink" href="#vcs-pull-requests-options"> ΒΆ</a></h3>
204 </div>
204 </div>
205 <div class="panel-body">
205 <div class="panel-body">
206 <div class="checkbox">
206 <div class="checkbox">
207 ${h.checkbox('rhodecode_pr_merge_enabled' + suffix, 'True', **kwargs)}
207 ${h.checkbox('rhodecode_pr_merge_enabled' + suffix, 'True', **kwargs)}
208 <label for="rhodecode_pr_merge_enabled${suffix}">${_('Enable server-side merge for pull requests')}</label>
208 <label for="rhodecode_pr_merge_enabled${suffix}">${_('Enable server-side merge for pull requests')}</label>
209 </div>
209 </div>
210 <div class="label">
210 <div class="label">
211 <span class="help-block">${_('Note: when this feature is enabled, it only runs hooks defined in the rcextension package. Custom hooks added on the Admin -> Settings -> Hooks page will not be run when pull requests are automatically merged from the web interface.')}</span>
211 <span class="help-block">${_('Note: when this feature is enabled, it only runs hooks defined in the rcextension package. Custom hooks added on the Admin -> Settings -> Hooks page will not be run when pull requests are automatically merged from the web interface.')}</span>
212 </div>
212 </div>
213 %if c.rhodecode_edition_id != 'EE':
214 <div class="checkbox">
215 <input type="checkbox" disabled>
216 <label for="rhodecode_auto_merge_enabled${suffix}">${_('Enable automatic merge for approved pull requests')}</label>
217 </div>
218 <div class="label">
219 <span class="help-block">${_('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='<a href="mailto:sales@rhodecode.com">sales@rhodecode.com</a>')|n}</span>
220 <div>
221 %else:
222 <div class="checkbox">
223 ${h.checkbox('rhodecode_auto_merge_enabled' + suffix, 'True', **kwargs)}
224 <label for="rhodecode_auto_merge_enabled${suffix}">${_('Enable automatic merge for approved pull requests')}</label>
225 </div>
226 <div class="label">
227 <span class="help-block">${_('When this is enabled, the pull request will be merged once it has at least one reviewer and is approved.')}</span>
228 </div>
229 %endif
213 <div class="checkbox">
230 <div class="checkbox">
214 ${h.checkbox('rhodecode_use_outdated_comments' + suffix, 'True', **kwargs)}
231 ${h.checkbox('rhodecode_use_outdated_comments' + suffix, 'True', **kwargs)}
215 <label for="rhodecode_use_outdated_comments${suffix}">${_('Invalidate and relocate inline comments during update')}</label>
232 <label for="rhodecode_use_outdated_comments${suffix}">${_('Invalidate and relocate inline comments during update')}</label>
216 </div>
233 </div>
217 <div class="label">
234 <div class="label">
218 <span class="help-block">${_('During the update of a pull request, the position of inline comments will be updated and outdated inline comments will be hidden.')}</span>
235 <span class="help-block">${_('During the update of a pull request, the position of inline comments will be updated and outdated inline comments will be hidden.')}</span>
219 </div>
236 </div>
220 </div>
237 </div>
221 </div>
238 </div>
222 % endif
239 % endif
223
240
224 % if display_globals or repo_type in ['hg', 'git', 'svn']:
241 % if display_globals or repo_type in ['hg', 'git', 'svn']:
225 <div class="panel panel-default">
242 <div class="panel panel-default">
226 <div class="panel-heading" id="vcs-pull-requests-options">
243 <div class="panel-heading" id="vcs-pull-requests-options">
227 <h3 class="panel-title">${_('Diff cache')}<a class="permalink" href="#vcs-pull-requests-options"> ΒΆ</a></h3>
244 <h3 class="panel-title">${_('Diff cache')}<a class="permalink" href="#vcs-pull-requests-options"> ΒΆ</a></h3>
228 </div>
245 </div>
229 <div class="panel-body">
246 <div class="panel-body">
230 <div class="checkbox">
247 <div class="checkbox">
231 ${h.checkbox('rhodecode_diff_cache' + suffix, 'True', **kwargs)}
248 ${h.checkbox('rhodecode_diff_cache' + suffix, 'True', **kwargs)}
232 <label for="rhodecode_diff_cache${suffix}">${_('Enable caching diffs for pull requests cache and commits')}</label>
249 <label for="rhodecode_diff_cache${suffix}">${_('Enable caching diffs for pull requests cache and commits')}</label>
233 </div>
250 </div>
234 </div>
251 </div>
235 </div>
252 </div>
236 % endif
253 % endif
237
254
238 % if display_globals or repo_type in ['hg',]:
255 % if display_globals or repo_type in ['hg',]:
239 <div class="panel panel-default">
256 <div class="panel panel-default">
240 <div class="panel-heading" id="vcs-pull-requests-options">
257 <div class="panel-heading" id="vcs-pull-requests-options">
241 <h3 class="panel-title">${_('Mercurial Pull Request Settings')}<a class="permalink" href="#vcs-hg-pull-requests-options"> ΒΆ</a></h3>
258 <h3 class="panel-title">${_('Mercurial Pull Request Settings')}<a class="permalink" href="#vcs-hg-pull-requests-options"> ΒΆ</a></h3>
242 </div>
259 </div>
243 <div class="panel-body">
260 <div class="panel-body">
244 ## Specific HG settings
261 ## Specific HG settings
245 <div class="checkbox">
262 <div class="checkbox">
246 ${h.checkbox('rhodecode_hg_use_rebase_for_merging' + suffix, 'True', **kwargs)}
263 ${h.checkbox('rhodecode_hg_use_rebase_for_merging' + suffix, 'True', **kwargs)}
247 <label for="rhodecode_hg_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
264 <label for="rhodecode_hg_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
248 </div>
265 </div>
249 <div class="label">
266 <div class="label">
250 <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
267 <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
251 </div>
268 </div>
252
269
253 <div class="checkbox">
270 <div class="checkbox">
254 ${h.checkbox('rhodecode_hg_close_branch_before_merging' + suffix, 'True', **kwargs)}
271 ${h.checkbox('rhodecode_hg_close_branch_before_merging' + suffix, 'True', **kwargs)}
255 <label for="rhodecode_hg_close_branch_before_merging{suffix}">${_('Close branch before merging it')}</label>
272 <label for="rhodecode_hg_close_branch_before_merging{suffix}">${_('Close branch before merging it')}</label>
256 </div>
273 </div>
257 <div class="label">
274 <div class="label">
258 <span class="help-block">${_('Close branch before merging it into destination branch. No effect when rebase strategy is use.')}</span>
275 <span class="help-block">${_('Close branch before merging it into destination branch. No effect when rebase strategy is use.')}</span>
259 </div>
276 </div>
260
277
261
278
262 </div>
279 </div>
263 </div>
280 </div>
264 % endif
281 % endif
265
282
266 % if display_globals or repo_type in ['git']:
283 % if display_globals or repo_type in ['git']:
267 <div class="panel panel-default">
284 <div class="panel panel-default">
268 <div class="panel-heading" id="vcs-pull-requests-options">
285 <div class="panel-heading" id="vcs-pull-requests-options">
269 <h3 class="panel-title">${_('Git Pull Request Settings')}<a class="permalink" href="#vcs-git-pull-requests-options"> ΒΆ</a></h3>
286 <h3 class="panel-title">${_('Git Pull Request Settings')}<a class="permalink" href="#vcs-git-pull-requests-options"> ΒΆ</a></h3>
270 </div>
287 </div>
271 <div class="panel-body">
288 <div class="panel-body">
272 ## <div class="checkbox">
289 ## <div class="checkbox">
273 ## ${h.checkbox('rhodecode_git_use_rebase_for_merging' + suffix, 'True', **kwargs)}
290 ## ${h.checkbox('rhodecode_git_use_rebase_for_merging' + suffix, 'True', **kwargs)}
274 ## <label for="rhodecode_git_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
291 ## <label for="rhodecode_git_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
275 ## </div>
292 ## </div>
276 ## <div class="label">
293 ## <div class="label">
277 ## <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
294 ## <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
278 ## </div>
295 ## </div>
279
296
280 <div class="checkbox">
297 <div class="checkbox">
281 ${h.checkbox('rhodecode_git_close_branch_before_merging' + suffix, 'True', **kwargs)}
298 ${h.checkbox('rhodecode_git_close_branch_before_merging' + suffix, 'True', **kwargs)}
282 <label for="rhodecode_git_close_branch_before_merging{suffix}">${_('Delete branch after merging it')}</label>
299 <label for="rhodecode_git_close_branch_before_merging{suffix}">${_('Delete branch after merging it')}</label>
283 </div>
300 </div>
284 <div class="label">
301 <div class="label">
285 <span class="help-block">${_('Delete branch after merging it into destination branch.')}</span>
302 <span class="help-block">${_('Delete branch after merging it into destination branch.')}</span>
286 </div>
303 </div>
287 </div>
304 </div>
288 </div>
305 </div>
289 % endif
306 % endif
290
307
291 <script type="text/javascript">
308 <script type="text/javascript">
292
309
293 $(document).ready(function() {
310 $(document).ready(function() {
294 /* On click handler for the `Generate Apache Config` button. It sends a
311 /* On click handler for the `Generate Apache Config` button. It sends a
295 POST request to trigger the (re)generation of the mod_dav_svn config. */
312 POST request to trigger the (re)generation of the mod_dav_svn config. */
296 $('#vcs_svn_generate_cfg').on('click', function(event) {
313 $('#vcs_svn_generate_cfg').on('click', function(event) {
297 event.preventDefault();
314 event.preventDefault();
298 var url = "${h.route_path('admin_settings_vcs_svn_generate_cfg')}";
315 var url = "${h.route_path('admin_settings_vcs_svn_generate_cfg')}";
299 var jqxhr = $.post(url, {'csrf_token': CSRF_TOKEN});
316 var jqxhr = $.post(url, {'csrf_token': CSRF_TOKEN});
300 jqxhr.done(function(data) {
317 jqxhr.done(function(data) {
301 $.Topic('/notifications').publish(data);
318 $.Topic('/notifications').publish(data);
302 });
319 });
303 });
320 });
304 });
321 });
305
322
306 </script>
323 </script>
307 </%def>
324 </%def>
308
325
@@ -1,1097 +1,1099
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib.utils2 import str2bool
23 from rhodecode.lib.utils2 import str2bool
24 from rhodecode.model.meta import Session
24 from rhodecode.model.meta import Session
25 from rhodecode.model.settings import VcsSettingsModel, UiSetting
25 from rhodecode.model.settings import VcsSettingsModel, UiSetting
26
26
27
27
28 HOOKS_FORM_DATA = {
28 HOOKS_FORM_DATA = {
29 'hooks_changegroup_repo_size': True,
29 'hooks_changegroup_repo_size': True,
30 'hooks_changegroup_push_logger': True,
30 'hooks_changegroup_push_logger': True,
31 'hooks_outgoing_pull_logger': True
31 'hooks_outgoing_pull_logger': True
32 }
32 }
33
33
34 SVN_FORM_DATA = {
34 SVN_FORM_DATA = {
35 'new_svn_branch': 'test-branch',
35 'new_svn_branch': 'test-branch',
36 'new_svn_tag': 'test-tag'
36 'new_svn_tag': 'test-tag'
37 }
37 }
38
38
39 GENERAL_FORM_DATA = {
39 GENERAL_FORM_DATA = {
40 'rhodecode_pr_merge_enabled': True,
40 'rhodecode_pr_merge_enabled': True,
41 'rhodecode_auto_merge_enabled': True,
41 'rhodecode_use_outdated_comments': True,
42 'rhodecode_use_outdated_comments': True,
42 'rhodecode_hg_use_rebase_for_merging': True,
43 'rhodecode_hg_use_rebase_for_merging': True,
43 'rhodecode_hg_close_branch_before_merging': True,
44 'rhodecode_hg_close_branch_before_merging': True,
44 'rhodecode_git_use_rebase_for_merging': True,
45 'rhodecode_git_use_rebase_for_merging': True,
45 'rhodecode_git_close_branch_before_merging': True,
46 'rhodecode_git_close_branch_before_merging': True,
46 'rhodecode_diff_cache': True,
47 'rhodecode_diff_cache': True,
47 }
48 }
48
49
49
50
50 class TestInheritGlobalSettingsProperty(object):
51 class TestInheritGlobalSettingsProperty(object):
51 def test_get_raises_exception_when_repository_not_specified(self):
52 def test_get_raises_exception_when_repository_not_specified(self):
52 model = VcsSettingsModel()
53 model = VcsSettingsModel()
53 with pytest.raises(Exception) as exc_info:
54 with pytest.raises(Exception) as exc_info:
54 model.inherit_global_settings
55 model.inherit_global_settings
55 assert str(exc_info.value) == 'Repository is not specified'
56 assert str(exc_info.value) == 'Repository is not specified'
56
57
57 def test_true_is_returned_when_value_is_not_found(self, repo_stub):
58 def test_true_is_returned_when_value_is_not_found(self, repo_stub):
58 model = VcsSettingsModel(repo=repo_stub.repo_name)
59 model = VcsSettingsModel(repo=repo_stub.repo_name)
59 assert model.inherit_global_settings is True
60 assert model.inherit_global_settings is True
60
61
61 def test_value_is_returned(self, repo_stub, settings_util):
62 def test_value_is_returned(self, repo_stub, settings_util):
62 model = VcsSettingsModel(repo=repo_stub.repo_name)
63 model = VcsSettingsModel(repo=repo_stub.repo_name)
63 settings_util.create_repo_rhodecode_setting(
64 settings_util.create_repo_rhodecode_setting(
64 repo_stub, VcsSettingsModel.INHERIT_SETTINGS, False, 'bool')
65 repo_stub, VcsSettingsModel.INHERIT_SETTINGS, False, 'bool')
65 assert model.inherit_global_settings is False
66 assert model.inherit_global_settings is False
66
67
67 def test_value_is_set(self, repo_stub):
68 def test_value_is_set(self, repo_stub):
68 model = VcsSettingsModel(repo=repo_stub.repo_name)
69 model = VcsSettingsModel(repo=repo_stub.repo_name)
69 model.inherit_global_settings = False
70 model.inherit_global_settings = False
70 setting = model.repo_settings.get_setting_by_name(
71 setting = model.repo_settings.get_setting_by_name(
71 VcsSettingsModel.INHERIT_SETTINGS)
72 VcsSettingsModel.INHERIT_SETTINGS)
72 try:
73 try:
73 assert setting.app_settings_type == 'bool'
74 assert setting.app_settings_type == 'bool'
74 assert setting.app_settings_value is False
75 assert setting.app_settings_value is False
75 finally:
76 finally:
76 Session().delete(setting)
77 Session().delete(setting)
77 Session().commit()
78 Session().commit()
78
79
79 def test_set_raises_exception_when_repository_not_specified(self):
80 def test_set_raises_exception_when_repository_not_specified(self):
80 model = VcsSettingsModel()
81 model = VcsSettingsModel()
81 with pytest.raises(Exception) as exc_info:
82 with pytest.raises(Exception) as exc_info:
82 model.inherit_global_settings = False
83 model.inherit_global_settings = False
83 assert str(exc_info.value) == 'Repository is not specified'
84 assert str(exc_info.value) == 'Repository is not specified'
84
85
85
86
86 class TestVcsSettingsModel(object):
87 class TestVcsSettingsModel(object):
87 def test_global_svn_branch_patterns(self):
88 def test_global_svn_branch_patterns(self):
88 model = VcsSettingsModel()
89 model = VcsSettingsModel()
89 expected_result = {'test': 'test'}
90 expected_result = {'test': 'test'}
90 with mock.patch.object(model, 'global_settings') as settings_mock:
91 with mock.patch.object(model, 'global_settings') as settings_mock:
91 get_settings = settings_mock.get_ui_by_section
92 get_settings = settings_mock.get_ui_by_section
92 get_settings.return_value = expected_result
93 get_settings.return_value = expected_result
93 settings_mock.return_value = expected_result
94 settings_mock.return_value = expected_result
94 result = model.get_global_svn_branch_patterns()
95 result = model.get_global_svn_branch_patterns()
95
96
96 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
97 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
97 assert expected_result == result
98 assert expected_result == result
98
99
99 def test_repo_svn_branch_patterns(self):
100 def test_repo_svn_branch_patterns(self):
100 model = VcsSettingsModel()
101 model = VcsSettingsModel()
101 expected_result = {'test': 'test'}
102 expected_result = {'test': 'test'}
102 with mock.patch.object(model, 'repo_settings') as settings_mock:
103 with mock.patch.object(model, 'repo_settings') as settings_mock:
103 get_settings = settings_mock.get_ui_by_section
104 get_settings = settings_mock.get_ui_by_section
104 get_settings.return_value = expected_result
105 get_settings.return_value = expected_result
105 settings_mock.return_value = expected_result
106 settings_mock.return_value = expected_result
106 result = model.get_repo_svn_branch_patterns()
107 result = model.get_repo_svn_branch_patterns()
107
108
108 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
109 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
109 assert expected_result == result
110 assert expected_result == result
110
111
111 def test_repo_svn_branch_patterns_raises_exception_when_repo_is_not_set(
112 def test_repo_svn_branch_patterns_raises_exception_when_repo_is_not_set(
112 self):
113 self):
113 model = VcsSettingsModel()
114 model = VcsSettingsModel()
114 with pytest.raises(Exception) as exc_info:
115 with pytest.raises(Exception) as exc_info:
115 model.get_repo_svn_branch_patterns()
116 model.get_repo_svn_branch_patterns()
116 assert str(exc_info.value) == 'Repository is not specified'
117 assert str(exc_info.value) == 'Repository is not specified'
117
118
118 def test_global_svn_tag_patterns(self):
119 def test_global_svn_tag_patterns(self):
119 model = VcsSettingsModel()
120 model = VcsSettingsModel()
120 expected_result = {'test': 'test'}
121 expected_result = {'test': 'test'}
121 with mock.patch.object(model, 'global_settings') as settings_mock:
122 with mock.patch.object(model, 'global_settings') as settings_mock:
122 get_settings = settings_mock.get_ui_by_section
123 get_settings = settings_mock.get_ui_by_section
123 get_settings.return_value = expected_result
124 get_settings.return_value = expected_result
124 settings_mock.return_value = expected_result
125 settings_mock.return_value = expected_result
125 result = model.get_global_svn_tag_patterns()
126 result = model.get_global_svn_tag_patterns()
126
127
127 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
128 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
128 assert expected_result == result
129 assert expected_result == result
129
130
130 def test_repo_svn_tag_patterns(self):
131 def test_repo_svn_tag_patterns(self):
131 model = VcsSettingsModel()
132 model = VcsSettingsModel()
132 expected_result = {'test': 'test'}
133 expected_result = {'test': 'test'}
133 with mock.patch.object(model, 'repo_settings') as settings_mock:
134 with mock.patch.object(model, 'repo_settings') as settings_mock:
134 get_settings = settings_mock.get_ui_by_section
135 get_settings = settings_mock.get_ui_by_section
135 get_settings.return_value = expected_result
136 get_settings.return_value = expected_result
136 settings_mock.return_value = expected_result
137 settings_mock.return_value = expected_result
137 result = model.get_repo_svn_tag_patterns()
138 result = model.get_repo_svn_tag_patterns()
138
139
139 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
140 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
140 assert expected_result == result
141 assert expected_result == result
141
142
142 def test_repo_svn_tag_patterns_raises_exception_when_repo_is_not_set(self):
143 def test_repo_svn_tag_patterns_raises_exception_when_repo_is_not_set(self):
143 model = VcsSettingsModel()
144 model = VcsSettingsModel()
144 with pytest.raises(Exception) as exc_info:
145 with pytest.raises(Exception) as exc_info:
145 model.get_repo_svn_tag_patterns()
146 model.get_repo_svn_tag_patterns()
146 assert str(exc_info.value) == 'Repository is not specified'
147 assert str(exc_info.value) == 'Repository is not specified'
147
148
148 def test_get_global_settings(self):
149 def test_get_global_settings(self):
149 expected_result = {'test': 'test'}
150 expected_result = {'test': 'test'}
150 model = VcsSettingsModel()
151 model = VcsSettingsModel()
151 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
152 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
152 collect_mock.return_value = expected_result
153 collect_mock.return_value = expected_result
153 result = model.get_global_settings()
154 result = model.get_global_settings()
154
155
155 collect_mock.assert_called_once_with(global_=True)
156 collect_mock.assert_called_once_with(global_=True)
156 assert result == expected_result
157 assert result == expected_result
157
158
158 def test_get_repo_settings(self, repo_stub):
159 def test_get_repo_settings(self, repo_stub):
159 model = VcsSettingsModel(repo=repo_stub.repo_name)
160 model = VcsSettingsModel(repo=repo_stub.repo_name)
160 expected_result = {'test': 'test'}
161 expected_result = {'test': 'test'}
161 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
162 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
162 collect_mock.return_value = expected_result
163 collect_mock.return_value = expected_result
163 result = model.get_repo_settings()
164 result = model.get_repo_settings()
164
165
165 collect_mock.assert_called_once_with(global_=False)
166 collect_mock.assert_called_once_with(global_=False)
166 assert result == expected_result
167 assert result == expected_result
167
168
168 @pytest.mark.parametrize('settings, global_', [
169 @pytest.mark.parametrize('settings, global_', [
169 ('global_settings', True),
170 ('global_settings', True),
170 ('repo_settings', False)
171 ('repo_settings', False)
171 ])
172 ])
172 def test_collect_all_settings(self, settings, global_):
173 def test_collect_all_settings(self, settings, global_):
173 model = VcsSettingsModel()
174 model = VcsSettingsModel()
174 result_mock = self._mock_result()
175 result_mock = self._mock_result()
175
176
176 settings_patch = mock.patch.object(model, settings)
177 settings_patch = mock.patch.object(model, settings)
177 with settings_patch as settings_mock:
178 with settings_patch as settings_mock:
178 settings_mock.get_ui_by_section_and_key.return_value = result_mock
179 settings_mock.get_ui_by_section_and_key.return_value = result_mock
179 settings_mock.get_setting_by_name.return_value = result_mock
180 settings_mock.get_setting_by_name.return_value = result_mock
180 result = model._collect_all_settings(global_=global_)
181 result = model._collect_all_settings(global_=global_)
181
182
182 ui_settings = model.HG_SETTINGS + model.GIT_SETTINGS + model.HOOKS_SETTINGS
183 ui_settings = model.HG_SETTINGS + model.GIT_SETTINGS + model.HOOKS_SETTINGS
183 self._assert_get_settings_calls(
184 self._assert_get_settings_calls(
184 settings_mock, ui_settings, model.GENERAL_SETTINGS)
185 settings_mock, ui_settings, model.GENERAL_SETTINGS)
185 self._assert_collect_all_settings_result(
186 self._assert_collect_all_settings_result(
186 ui_settings, model.GENERAL_SETTINGS, result)
187 ui_settings, model.GENERAL_SETTINGS, result)
187
188
188 @pytest.mark.parametrize('settings, global_', [
189 @pytest.mark.parametrize('settings, global_', [
189 ('global_settings', True),
190 ('global_settings', True),
190 ('repo_settings', False)
191 ('repo_settings', False)
191 ])
192 ])
192 def test_collect_all_settings_without_empty_value(self, settings, global_):
193 def test_collect_all_settings_without_empty_value(self, settings, global_):
193 model = VcsSettingsModel()
194 model = VcsSettingsModel()
194
195
195 settings_patch = mock.patch.object(model, settings)
196 settings_patch = mock.patch.object(model, settings)
196 with settings_patch as settings_mock:
197 with settings_patch as settings_mock:
197 settings_mock.get_ui_by_section_and_key.return_value = None
198 settings_mock.get_ui_by_section_and_key.return_value = None
198 settings_mock.get_setting_by_name.return_value = None
199 settings_mock.get_setting_by_name.return_value = None
199 result = model._collect_all_settings(global_=global_)
200 result = model._collect_all_settings(global_=global_)
200
201
201 assert result == {}
202 assert result == {}
202
203
203 def _mock_result(self):
204 def _mock_result(self):
204 result_mock = mock.Mock()
205 result_mock = mock.Mock()
205 result_mock.ui_value = 'ui_value'
206 result_mock.ui_value = 'ui_value'
206 result_mock.ui_active = True
207 result_mock.ui_active = True
207 result_mock.app_settings_value = 'setting_value'
208 result_mock.app_settings_value = 'setting_value'
208 return result_mock
209 return result_mock
209
210
210 def _assert_get_settings_calls(
211 def _assert_get_settings_calls(
211 self, settings_mock, ui_settings, general_settings):
212 self, settings_mock, ui_settings, general_settings):
212 assert (
213 assert (
213 settings_mock.get_ui_by_section_and_key.call_count ==
214 settings_mock.get_ui_by_section_and_key.call_count ==
214 len(ui_settings))
215 len(ui_settings))
215 assert (
216 assert (
216 settings_mock.get_setting_by_name.call_count ==
217 settings_mock.get_setting_by_name.call_count ==
217 len(general_settings))
218 len(general_settings))
218
219
219 for section, key in ui_settings:
220 for section, key in ui_settings:
220 expected_call = mock.call(section, key)
221 expected_call = mock.call(section, key)
221 assert (
222 assert (
222 expected_call in
223 expected_call in
223 settings_mock.get_ui_by_section_and_key.call_args_list)
224 settings_mock.get_ui_by_section_and_key.call_args_list)
224
225
225 for name in general_settings:
226 for name in general_settings:
226 expected_call = mock.call(name)
227 expected_call = mock.call(name)
227 assert (
228 assert (
228 expected_call in
229 expected_call in
229 settings_mock.get_setting_by_name.call_args_list)
230 settings_mock.get_setting_by_name.call_args_list)
230
231
231 def _assert_collect_all_settings_result(
232 def _assert_collect_all_settings_result(
232 self, ui_settings, general_settings, result):
233 self, ui_settings, general_settings, result):
233 expected_result = {}
234 expected_result = {}
234 for section, key in ui_settings:
235 for section, key in ui_settings:
235 key = '{}_{}'.format(section, key.replace('.', '_'))
236 key = '{}_{}'.format(section, key.replace('.', '_'))
236
237
237 if section in ('extensions', 'hooks'):
238 if section in ('extensions', 'hooks'):
238 value = True
239 value = True
239 elif key in ['vcs_git_lfs_enabled']:
240 elif key in ['vcs_git_lfs_enabled']:
240 value = True
241 value = True
241 else:
242 else:
242 value = 'ui_value'
243 value = 'ui_value'
243 expected_result[key] = value
244 expected_result[key] = value
244
245
245 for name in general_settings:
246 for name in general_settings:
246 key = 'rhodecode_' + name
247 key = 'rhodecode_' + name
247 expected_result[key] = 'setting_value'
248 expected_result[key] = 'setting_value'
248
249
249 assert expected_result == result
250 assert expected_result == result
250
251
251
252
252 class TestCreateOrUpdateRepoHookSettings(object):
253 class TestCreateOrUpdateRepoHookSettings(object):
253 def test_create_when_no_repo_object_found(self, repo_stub):
254 def test_create_when_no_repo_object_found(self, repo_stub):
254 model = VcsSettingsModel(repo=repo_stub.repo_name)
255 model = VcsSettingsModel(repo=repo_stub.repo_name)
255
256
256 self._create_settings(model, HOOKS_FORM_DATA)
257 self._create_settings(model, HOOKS_FORM_DATA)
257
258
258 cleanup = []
259 cleanup = []
259 try:
260 try:
260 for section, key in model.HOOKS_SETTINGS:
261 for section, key in model.HOOKS_SETTINGS:
261 ui = model.repo_settings.get_ui_by_section_and_key(
262 ui = model.repo_settings.get_ui_by_section_and_key(
262 section, key)
263 section, key)
263 assert ui.ui_active is True
264 assert ui.ui_active is True
264 cleanup.append(ui)
265 cleanup.append(ui)
265 finally:
266 finally:
266 for ui in cleanup:
267 for ui in cleanup:
267 Session().delete(ui)
268 Session().delete(ui)
268 Session().commit()
269 Session().commit()
269
270
270 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
271 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
271 model = VcsSettingsModel(repo=repo_stub.repo_name)
272 model = VcsSettingsModel(repo=repo_stub.repo_name)
272
273
273 deleted_key = 'hooks_changegroup_repo_size'
274 deleted_key = 'hooks_changegroup_repo_size'
274 data = HOOKS_FORM_DATA.copy()
275 data = HOOKS_FORM_DATA.copy()
275 data.pop(deleted_key)
276 data.pop(deleted_key)
276
277
277 with pytest.raises(ValueError) as exc_info:
278 with pytest.raises(ValueError) as exc_info:
278 model.create_or_update_repo_hook_settings(data)
279 model.create_or_update_repo_hook_settings(data)
279 Session().commit()
280 Session().commit()
280
281
281 msg = 'The given data does not contain {} key'.format(deleted_key)
282 msg = 'The given data does not contain {} key'.format(deleted_key)
282 assert str(exc_info.value) == msg
283 assert str(exc_info.value) == msg
283
284
284 def test_update_when_repo_object_found(self, repo_stub, settings_util):
285 def test_update_when_repo_object_found(self, repo_stub, settings_util):
285 model = VcsSettingsModel(repo=repo_stub.repo_name)
286 model = VcsSettingsModel(repo=repo_stub.repo_name)
286 for section, key in model.HOOKS_SETTINGS:
287 for section, key in model.HOOKS_SETTINGS:
287 settings_util.create_repo_rhodecode_ui(
288 settings_util.create_repo_rhodecode_ui(
288 repo_stub, section, None, key=key, active=False)
289 repo_stub, section, None, key=key, active=False)
289 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
290 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
290 Session().commit()
291 Session().commit()
291
292
292 for section, key in model.HOOKS_SETTINGS:
293 for section, key in model.HOOKS_SETTINGS:
293 ui = model.repo_settings.get_ui_by_section_and_key(section, key)
294 ui = model.repo_settings.get_ui_by_section_and_key(section, key)
294 assert ui.ui_active is True
295 assert ui.ui_active is True
295
296
296 def _create_settings(self, model, data):
297 def _create_settings(self, model, data):
297 global_patch = mock.patch.object(model, 'global_settings')
298 global_patch = mock.patch.object(model, 'global_settings')
298 global_setting = mock.Mock()
299 global_setting = mock.Mock()
299 global_setting.ui_value = 'Test value'
300 global_setting.ui_value = 'Test value'
300 with global_patch as global_mock:
301 with global_patch as global_mock:
301 global_mock.get_ui_by_section_and_key.return_value = global_setting
302 global_mock.get_ui_by_section_and_key.return_value = global_setting
302 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
303 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
303 Session().commit()
304 Session().commit()
304
305
305
306
306 class TestUpdateGlobalHookSettings(object):
307 class TestUpdateGlobalHookSettings(object):
307 def test_update_raises_exception_when_data_incomplete(self):
308 def test_update_raises_exception_when_data_incomplete(self):
308 model = VcsSettingsModel()
309 model = VcsSettingsModel()
309
310
310 deleted_key = 'hooks_changegroup_repo_size'
311 deleted_key = 'hooks_changegroup_repo_size'
311 data = HOOKS_FORM_DATA.copy()
312 data = HOOKS_FORM_DATA.copy()
312 data.pop(deleted_key)
313 data.pop(deleted_key)
313
314
314 with pytest.raises(ValueError) as exc_info:
315 with pytest.raises(ValueError) as exc_info:
315 model.update_global_hook_settings(data)
316 model.update_global_hook_settings(data)
316 Session().commit()
317 Session().commit()
317
318
318 msg = 'The given data does not contain {} key'.format(deleted_key)
319 msg = 'The given data does not contain {} key'.format(deleted_key)
319 assert str(exc_info.value) == msg
320 assert str(exc_info.value) == msg
320
321
321 def test_update_global_hook_settings(self, settings_util):
322 def test_update_global_hook_settings(self, settings_util):
322 model = VcsSettingsModel()
323 model = VcsSettingsModel()
323 setting_mock = mock.MagicMock()
324 setting_mock = mock.MagicMock()
324 setting_mock.ui_active = False
325 setting_mock.ui_active = False
325 get_settings_patcher = mock.patch.object(
326 get_settings_patcher = mock.patch.object(
326 model.global_settings, 'get_ui_by_section_and_key',
327 model.global_settings, 'get_ui_by_section_and_key',
327 return_value=setting_mock)
328 return_value=setting_mock)
328 session_patcher = mock.patch('rhodecode.model.settings.Session')
329 session_patcher = mock.patch('rhodecode.model.settings.Session')
329 with get_settings_patcher as get_settings_mock, session_patcher:
330 with get_settings_patcher as get_settings_mock, session_patcher:
330 model.update_global_hook_settings(HOOKS_FORM_DATA)
331 model.update_global_hook_settings(HOOKS_FORM_DATA)
331 Session().commit()
332 Session().commit()
332
333
333 assert setting_mock.ui_active is True
334 assert setting_mock.ui_active is True
334 assert get_settings_mock.call_count == 3
335 assert get_settings_mock.call_count == 3
335
336
336
337
337 class TestCreateOrUpdateRepoGeneralSettings(object):
338 class TestCreateOrUpdateRepoGeneralSettings(object):
338 def test_calls_create_or_update_general_settings(self, repo_stub):
339 def test_calls_create_or_update_general_settings(self, repo_stub):
339 model = VcsSettingsModel(repo=repo_stub.repo_name)
340 model = VcsSettingsModel(repo=repo_stub.repo_name)
340 create_patch = mock.patch.object(
341 create_patch = mock.patch.object(
341 model, '_create_or_update_general_settings')
342 model, '_create_or_update_general_settings')
342 with create_patch as create_mock:
343 with create_patch as create_mock:
343 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
344 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
344 Session().commit()
345 Session().commit()
345
346
346 create_mock.assert_called_once_with(
347 create_mock.assert_called_once_with(
347 model.repo_settings, GENERAL_FORM_DATA)
348 model.repo_settings, GENERAL_FORM_DATA)
348
349
349 def test_raises_exception_when_repository_is_not_specified(self):
350 def test_raises_exception_when_repository_is_not_specified(self):
350 model = VcsSettingsModel()
351 model = VcsSettingsModel()
351 with pytest.raises(Exception) as exc_info:
352 with pytest.raises(Exception) as exc_info:
352 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
353 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
353 assert str(exc_info.value) == 'Repository is not specified'
354 assert str(exc_info.value) == 'Repository is not specified'
354
355
355
356
356 class TestCreateOrUpdatGlobalGeneralSettings(object):
357 class TestCreateOrUpdatGlobalGeneralSettings(object):
357 def test_calls_create_or_update_general_settings(self):
358 def test_calls_create_or_update_general_settings(self):
358 model = VcsSettingsModel()
359 model = VcsSettingsModel()
359 create_patch = mock.patch.object(
360 create_patch = mock.patch.object(
360 model, '_create_or_update_general_settings')
361 model, '_create_or_update_general_settings')
361 with create_patch as create_mock:
362 with create_patch as create_mock:
362 model.create_or_update_global_pr_settings(GENERAL_FORM_DATA)
363 model.create_or_update_global_pr_settings(GENERAL_FORM_DATA)
363 create_mock.assert_called_once_with(
364 create_mock.assert_called_once_with(
364 model.global_settings, GENERAL_FORM_DATA)
365 model.global_settings, GENERAL_FORM_DATA)
365
366
366
367
367 class TestCreateOrUpdateGeneralSettings(object):
368 class TestCreateOrUpdateGeneralSettings(object):
368 def test_create_when_no_repo_settings_found(self, repo_stub):
369 def test_create_when_no_repo_settings_found(self, repo_stub):
369 model = VcsSettingsModel(repo=repo_stub.repo_name)
370 model = VcsSettingsModel(repo=repo_stub.repo_name)
370 model._create_or_update_general_settings(
371 model._create_or_update_general_settings(
371 model.repo_settings, GENERAL_FORM_DATA)
372 model.repo_settings, GENERAL_FORM_DATA)
372
373
373 cleanup = []
374 cleanup = []
374 try:
375 try:
375 for name in model.GENERAL_SETTINGS:
376 for name in model.GENERAL_SETTINGS:
376 setting = model.repo_settings.get_setting_by_name(name)
377 setting = model.repo_settings.get_setting_by_name(name)
377 assert setting.app_settings_value is True
378 assert setting.app_settings_value is True
378 cleanup.append(setting)
379 cleanup.append(setting)
379 finally:
380 finally:
380 for setting in cleanup:
381 for setting in cleanup:
381 Session().delete(setting)
382 Session().delete(setting)
382 Session().commit()
383 Session().commit()
383
384
384 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
385 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
385 model = VcsSettingsModel(repo=repo_stub.repo_name)
386 model = VcsSettingsModel(repo=repo_stub.repo_name)
386
387
387 deleted_key = 'rhodecode_pr_merge_enabled'
388 deleted_key = 'rhodecode_pr_merge_enabled'
388 data = GENERAL_FORM_DATA.copy()
389 data = GENERAL_FORM_DATA.copy()
389 data.pop(deleted_key)
390 data.pop(deleted_key)
390
391
391 with pytest.raises(ValueError) as exc_info:
392 with pytest.raises(ValueError) as exc_info:
392 model._create_or_update_general_settings(model.repo_settings, data)
393 model._create_or_update_general_settings(model.repo_settings, data)
393 Session().commit()
394 Session().commit()
394
395
395 msg = 'The given data does not contain {} key'.format(deleted_key)
396 msg = 'The given data does not contain {} key'.format(deleted_key)
396 assert str(exc_info.value) == msg
397 assert str(exc_info.value) == msg
397
398
398 def test_update_when_repo_setting_found(self, repo_stub, settings_util):
399 def test_update_when_repo_setting_found(self, repo_stub, settings_util):
399 model = VcsSettingsModel(repo=repo_stub.repo_name)
400 model = VcsSettingsModel(repo=repo_stub.repo_name)
400 for name in model.GENERAL_SETTINGS:
401 for name in model.GENERAL_SETTINGS:
401 settings_util.create_repo_rhodecode_setting(
402 settings_util.create_repo_rhodecode_setting(
402 repo_stub, name, False, 'bool')
403 repo_stub, name, False, 'bool')
403
404
404 model._create_or_update_general_settings(
405 model._create_or_update_general_settings(
405 model.repo_settings, GENERAL_FORM_DATA)
406 model.repo_settings, GENERAL_FORM_DATA)
406 Session().commit()
407 Session().commit()
407
408
408 for name in model.GENERAL_SETTINGS:
409 for name in model.GENERAL_SETTINGS:
409 setting = model.repo_settings.get_setting_by_name(name)
410 setting = model.repo_settings.get_setting_by_name(name)
410 assert setting.app_settings_value is True
411 assert setting.app_settings_value is True
411
412
412
413
413 class TestCreateRepoSvnSettings(object):
414 class TestCreateRepoSvnSettings(object):
414 def test_calls_create_svn_settings(self, repo_stub):
415 def test_calls_create_svn_settings(self, repo_stub):
415 model = VcsSettingsModel(repo=repo_stub.repo_name)
416 model = VcsSettingsModel(repo=repo_stub.repo_name)
416 with mock.patch.object(model, '_create_svn_settings') as create_mock:
417 with mock.patch.object(model, '_create_svn_settings') as create_mock:
417 model.create_repo_svn_settings(SVN_FORM_DATA)
418 model.create_repo_svn_settings(SVN_FORM_DATA)
418 Session().commit()
419 Session().commit()
419
420
420 create_mock.assert_called_once_with(model.repo_settings, SVN_FORM_DATA)
421 create_mock.assert_called_once_with(model.repo_settings, SVN_FORM_DATA)
421
422
422 def test_raises_exception_when_repository_is_not_specified(self):
423 def test_raises_exception_when_repository_is_not_specified(self):
423 model = VcsSettingsModel()
424 model = VcsSettingsModel()
424 with pytest.raises(Exception) as exc_info:
425 with pytest.raises(Exception) as exc_info:
425 model.create_repo_svn_settings(SVN_FORM_DATA)
426 model.create_repo_svn_settings(SVN_FORM_DATA)
426 Session().commit()
427 Session().commit()
427
428
428 assert str(exc_info.value) == 'Repository is not specified'
429 assert str(exc_info.value) == 'Repository is not specified'
429
430
430
431
431 class TestCreateSvnSettings(object):
432 class TestCreateSvnSettings(object):
432 def test_create(self, repo_stub):
433 def test_create(self, repo_stub):
433 model = VcsSettingsModel(repo=repo_stub.repo_name)
434 model = VcsSettingsModel(repo=repo_stub.repo_name)
434 model._create_svn_settings(model.repo_settings, SVN_FORM_DATA)
435 model._create_svn_settings(model.repo_settings, SVN_FORM_DATA)
435 Session().commit()
436 Session().commit()
436
437
437 branch_ui = model.repo_settings.get_ui_by_section(
438 branch_ui = model.repo_settings.get_ui_by_section(
438 model.SVN_BRANCH_SECTION)
439 model.SVN_BRANCH_SECTION)
439 tag_ui = model.repo_settings.get_ui_by_section(
440 tag_ui = model.repo_settings.get_ui_by_section(
440 model.SVN_TAG_SECTION)
441 model.SVN_TAG_SECTION)
441
442
442 try:
443 try:
443 assert len(branch_ui) == 1
444 assert len(branch_ui) == 1
444 assert len(tag_ui) == 1
445 assert len(tag_ui) == 1
445 finally:
446 finally:
446 Session().delete(branch_ui[0])
447 Session().delete(branch_ui[0])
447 Session().delete(tag_ui[0])
448 Session().delete(tag_ui[0])
448 Session().commit()
449 Session().commit()
449
450
450 def test_create_tag(self, repo_stub):
451 def test_create_tag(self, repo_stub):
451 model = VcsSettingsModel(repo=repo_stub.repo_name)
452 model = VcsSettingsModel(repo=repo_stub.repo_name)
452 data = SVN_FORM_DATA.copy()
453 data = SVN_FORM_DATA.copy()
453 data.pop('new_svn_branch')
454 data.pop('new_svn_branch')
454 model._create_svn_settings(model.repo_settings, data)
455 model._create_svn_settings(model.repo_settings, data)
455 Session().commit()
456 Session().commit()
456
457
457 branch_ui = model.repo_settings.get_ui_by_section(
458 branch_ui = model.repo_settings.get_ui_by_section(
458 model.SVN_BRANCH_SECTION)
459 model.SVN_BRANCH_SECTION)
459 tag_ui = model.repo_settings.get_ui_by_section(
460 tag_ui = model.repo_settings.get_ui_by_section(
460 model.SVN_TAG_SECTION)
461 model.SVN_TAG_SECTION)
461
462
462 try:
463 try:
463 assert len(branch_ui) == 0
464 assert len(branch_ui) == 0
464 assert len(tag_ui) == 1
465 assert len(tag_ui) == 1
465 finally:
466 finally:
466 Session().delete(tag_ui[0])
467 Session().delete(tag_ui[0])
467 Session().commit()
468 Session().commit()
468
469
469 def test_create_nothing_when_no_svn_settings_specified(self, repo_stub):
470 def test_create_nothing_when_no_svn_settings_specified(self, repo_stub):
470 model = VcsSettingsModel(repo=repo_stub.repo_name)
471 model = VcsSettingsModel(repo=repo_stub.repo_name)
471 model._create_svn_settings(model.repo_settings, {})
472 model._create_svn_settings(model.repo_settings, {})
472 Session().commit()
473 Session().commit()
473
474
474 branch_ui = model.repo_settings.get_ui_by_section(
475 branch_ui = model.repo_settings.get_ui_by_section(
475 model.SVN_BRANCH_SECTION)
476 model.SVN_BRANCH_SECTION)
476 tag_ui = model.repo_settings.get_ui_by_section(
477 tag_ui = model.repo_settings.get_ui_by_section(
477 model.SVN_TAG_SECTION)
478 model.SVN_TAG_SECTION)
478
479
479 assert len(branch_ui) == 0
480 assert len(branch_ui) == 0
480 assert len(tag_ui) == 0
481 assert len(tag_ui) == 0
481
482
482 def test_create_nothing_when_empty_settings_specified(self, repo_stub):
483 def test_create_nothing_when_empty_settings_specified(self, repo_stub):
483 model = VcsSettingsModel(repo=repo_stub.repo_name)
484 model = VcsSettingsModel(repo=repo_stub.repo_name)
484 data = {
485 data = {
485 'new_svn_branch': '',
486 'new_svn_branch': '',
486 'new_svn_tag': ''
487 'new_svn_tag': ''
487 }
488 }
488 model._create_svn_settings(model.repo_settings, data)
489 model._create_svn_settings(model.repo_settings, data)
489 Session().commit()
490 Session().commit()
490
491
491 branch_ui = model.repo_settings.get_ui_by_section(
492 branch_ui = model.repo_settings.get_ui_by_section(
492 model.SVN_BRANCH_SECTION)
493 model.SVN_BRANCH_SECTION)
493 tag_ui = model.repo_settings.get_ui_by_section(
494 tag_ui = model.repo_settings.get_ui_by_section(
494 model.SVN_TAG_SECTION)
495 model.SVN_TAG_SECTION)
495
496
496 assert len(branch_ui) == 0
497 assert len(branch_ui) == 0
497 assert len(tag_ui) == 0
498 assert len(tag_ui) == 0
498
499
499
500
500 class TestCreateOrUpdateUi(object):
501 class TestCreateOrUpdateUi(object):
501 def test_create(self, repo_stub):
502 def test_create(self, repo_stub):
502 model = VcsSettingsModel(repo=repo_stub.repo_name)
503 model = VcsSettingsModel(repo=repo_stub.repo_name)
503 model._create_or_update_ui(
504 model._create_or_update_ui(
504 model.repo_settings, 'test-section', 'test-key', active=False,
505 model.repo_settings, 'test-section', 'test-key', active=False,
505 value='False')
506 value='False')
506 Session().commit()
507 Session().commit()
507
508
508 created_ui = model.repo_settings.get_ui_by_section_and_key(
509 created_ui = model.repo_settings.get_ui_by_section_and_key(
509 'test-section', 'test-key')
510 'test-section', 'test-key')
510
511
511 try:
512 try:
512 assert created_ui.ui_active is False
513 assert created_ui.ui_active is False
513 assert str2bool(created_ui.ui_value) is False
514 assert str2bool(created_ui.ui_value) is False
514 finally:
515 finally:
515 Session().delete(created_ui)
516 Session().delete(created_ui)
516 Session().commit()
517 Session().commit()
517
518
518 def test_update(self, repo_stub, settings_util):
519 def test_update(self, repo_stub, settings_util):
519 model = VcsSettingsModel(repo=repo_stub.repo_name)
520 model = VcsSettingsModel(repo=repo_stub.repo_name)
520 # care about only 3 first settings
521 # care about only 3 first settings
521 largefiles, phases, evolve = model.HG_SETTINGS[:3]
522 largefiles, phases, evolve = model.HG_SETTINGS[:3]
522
523
523 section = 'test-section'
524 section = 'test-section'
524 key = 'test-key'
525 key = 'test-key'
525 settings_util.create_repo_rhodecode_ui(
526 settings_util.create_repo_rhodecode_ui(
526 repo_stub, section, 'True', key=key, active=True)
527 repo_stub, section, 'True', key=key, active=True)
527
528
528 model._create_or_update_ui(
529 model._create_or_update_ui(
529 model.repo_settings, section, key, active=False, value='False')
530 model.repo_settings, section, key, active=False, value='False')
530 Session().commit()
531 Session().commit()
531
532
532 created_ui = model.repo_settings.get_ui_by_section_and_key(
533 created_ui = model.repo_settings.get_ui_by_section_and_key(
533 section, key)
534 section, key)
534 assert created_ui.ui_active is False
535 assert created_ui.ui_active is False
535 assert str2bool(created_ui.ui_value) is False
536 assert str2bool(created_ui.ui_value) is False
536
537
537
538
538 class TestCreateOrUpdateRepoHgSettings(object):
539 class TestCreateOrUpdateRepoHgSettings(object):
539 FORM_DATA = {
540 FORM_DATA = {
540 'extensions_largefiles': False,
541 'extensions_largefiles': False,
541 'extensions_evolve': False,
542 'extensions_evolve': False,
542 'phases_publish': False
543 'phases_publish': False
543 }
544 }
544
545
545 def test_creates_repo_hg_settings_when_data_is_correct(self, repo_stub):
546 def test_creates_repo_hg_settings_when_data_is_correct(self, repo_stub):
546 model = VcsSettingsModel(repo=repo_stub.repo_name)
547 model = VcsSettingsModel(repo=repo_stub.repo_name)
547 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
548 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
548 model.create_or_update_repo_hg_settings(self.FORM_DATA)
549 model.create_or_update_repo_hg_settings(self.FORM_DATA)
549 expected_calls = [
550 expected_calls = [
550 mock.call(model.repo_settings, 'extensions', 'largefiles', active=False, value=''),
551 mock.call(model.repo_settings, 'extensions', 'largefiles', active=False, value=''),
551 mock.call(model.repo_settings, 'extensions', 'evolve', active=False, value=''),
552 mock.call(model.repo_settings, 'extensions', 'evolve', active=False, value=''),
552 mock.call(model.repo_settings, 'experimental', 'evolution', active=False, value=''),
553 mock.call(model.repo_settings, 'experimental', 'evolution', active=False, value=''),
553 mock.call(model.repo_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
554 mock.call(model.repo_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
554 mock.call(model.repo_settings, 'extensions', 'topic', active=False, value=''),
555 mock.call(model.repo_settings, 'extensions', 'topic', active=False, value=''),
555 mock.call(model.repo_settings, 'phases', 'publish', value='False'),
556 mock.call(model.repo_settings, 'phases', 'publish', value='False'),
556 ]
557 ]
557 assert expected_calls == create_mock.call_args_list
558 assert expected_calls == create_mock.call_args_list
558
559
559 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
560 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
560 def test_key_is_not_found(self, repo_stub, field_to_remove):
561 def test_key_is_not_found(self, repo_stub, field_to_remove):
561 model = VcsSettingsModel(repo=repo_stub.repo_name)
562 model = VcsSettingsModel(repo=repo_stub.repo_name)
562 data = self.FORM_DATA.copy()
563 data = self.FORM_DATA.copy()
563 data.pop(field_to_remove)
564 data.pop(field_to_remove)
564 with pytest.raises(ValueError) as exc_info:
565 with pytest.raises(ValueError) as exc_info:
565 model.create_or_update_repo_hg_settings(data)
566 model.create_or_update_repo_hg_settings(data)
566 Session().commit()
567 Session().commit()
567
568
568 expected_message = 'The given data does not contain {} key'.format(
569 expected_message = 'The given data does not contain {} key'.format(
569 field_to_remove)
570 field_to_remove)
570 assert str(exc_info.value) == expected_message
571 assert str(exc_info.value) == expected_message
571
572
572 def test_create_raises_exception_when_repository_not_specified(self):
573 def test_create_raises_exception_when_repository_not_specified(self):
573 model = VcsSettingsModel()
574 model = VcsSettingsModel()
574 with pytest.raises(Exception) as exc_info:
575 with pytest.raises(Exception) as exc_info:
575 model.create_or_update_repo_hg_settings(self.FORM_DATA)
576 model.create_or_update_repo_hg_settings(self.FORM_DATA)
576 Session().commit()
577 Session().commit()
577
578
578 assert str(exc_info.value) == 'Repository is not specified'
579 assert str(exc_info.value) == 'Repository is not specified'
579
580
580
581
581 class TestCreateOrUpdateGlobalHgSettings(object):
582 class TestCreateOrUpdateGlobalHgSettings(object):
582 FORM_DATA = {
583 FORM_DATA = {
583 'extensions_largefiles': False,
584 'extensions_largefiles': False,
584 'phases_publish': False,
585 'phases_publish': False,
585 'extensions_evolve': False
586 'extensions_evolve': False
586 }
587 }
587
588
588 def test_creates_repo_hg_settings_when_data_is_correct(self):
589 def test_creates_repo_hg_settings_when_data_is_correct(self):
589 model = VcsSettingsModel()
590 model = VcsSettingsModel()
590 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
591 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
591 model.create_or_update_global_hg_settings(self.FORM_DATA)
592 model.create_or_update_global_hg_settings(self.FORM_DATA)
592 Session().commit()
593 Session().commit()
593
594
594 expected_calls = [
595 expected_calls = [
595 mock.call(model.global_settings, 'extensions', 'largefiles', active=False, value=''),
596 mock.call(model.global_settings, 'extensions', 'largefiles', active=False, value=''),
596 mock.call(model.global_settings, 'phases', 'publish', value='False'),
597 mock.call(model.global_settings, 'phases', 'publish', value='False'),
597 mock.call(model.global_settings, 'extensions', 'evolve', active=False, value=''),
598 mock.call(model.global_settings, 'extensions', 'evolve', active=False, value=''),
598 mock.call(model.global_settings, 'experimental', 'evolution', active=False, value=''),
599 mock.call(model.global_settings, 'experimental', 'evolution', active=False, value=''),
599 mock.call(model.global_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
600 mock.call(model.global_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
600 mock.call(model.global_settings, 'extensions', 'topic', active=False, value=''),
601 mock.call(model.global_settings, 'extensions', 'topic', active=False, value=''),
601 ]
602 ]
602
603
603 assert expected_calls == create_mock.call_args_list
604 assert expected_calls == create_mock.call_args_list
604
605
605 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
606 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
606 def test_key_is_not_found(self, repo_stub, field_to_remove):
607 def test_key_is_not_found(self, repo_stub, field_to_remove):
607 model = VcsSettingsModel(repo=repo_stub.repo_name)
608 model = VcsSettingsModel(repo=repo_stub.repo_name)
608 data = self.FORM_DATA.copy()
609 data = self.FORM_DATA.copy()
609 data.pop(field_to_remove)
610 data.pop(field_to_remove)
610 with pytest.raises(Exception) as exc_info:
611 with pytest.raises(Exception) as exc_info:
611 model.create_or_update_global_hg_settings(data)
612 model.create_or_update_global_hg_settings(data)
612 Session().commit()
613 Session().commit()
613
614
614 expected_message = 'The given data does not contain {} key'.format(
615 expected_message = 'The given data does not contain {} key'.format(
615 field_to_remove)
616 field_to_remove)
616 assert str(exc_info.value) == expected_message
617 assert str(exc_info.value) == expected_message
617
618
618
619
619 class TestCreateOrUpdateGlobalGitSettings(object):
620 class TestCreateOrUpdateGlobalGitSettings(object):
620 FORM_DATA = {
621 FORM_DATA = {
621 'vcs_git_lfs_enabled': False,
622 'vcs_git_lfs_enabled': False,
622 }
623 }
623
624
624 def test_creates_repo_hg_settings_when_data_is_correct(self):
625 def test_creates_repo_hg_settings_when_data_is_correct(self):
625 model = VcsSettingsModel()
626 model = VcsSettingsModel()
626 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
627 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
627 model.create_or_update_global_git_settings(self.FORM_DATA)
628 model.create_or_update_global_git_settings(self.FORM_DATA)
628 Session().commit()
629 Session().commit()
629
630
630 expected_calls = [
631 expected_calls = [
631 mock.call(model.global_settings, 'vcs_git_lfs', 'enabled', active=False, value=False),
632 mock.call(model.global_settings, 'vcs_git_lfs', 'enabled', active=False, value=False),
632 ]
633 ]
633 assert expected_calls == create_mock.call_args_list
634 assert expected_calls == create_mock.call_args_list
634
635
635
636
636 class TestDeleteRepoSvnPattern(object):
637 class TestDeleteRepoSvnPattern(object):
637 def test_success_when_repo_is_set(self, backend_svn, settings_util):
638 def test_success_when_repo_is_set(self, backend_svn, settings_util):
638 repo = backend_svn.create_repo()
639 repo = backend_svn.create_repo()
639 repo_name = repo.repo_name
640 repo_name = repo.repo_name
640
641
641 model = VcsSettingsModel(repo=repo_name)
642 model = VcsSettingsModel(repo=repo_name)
642 entry = settings_util.create_repo_rhodecode_ui(
643 entry = settings_util.create_repo_rhodecode_ui(
643 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch')
644 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch')
644 Session().commit()
645 Session().commit()
645
646
646 model.delete_repo_svn_pattern(entry.ui_id)
647 model.delete_repo_svn_pattern(entry.ui_id)
647
648
648 def test_fail_when_delete_id_from_other_repo(self, backend_svn):
649 def test_fail_when_delete_id_from_other_repo(self, backend_svn):
649 repo_name = backend_svn.repo_name
650 repo_name = backend_svn.repo_name
650 model = VcsSettingsModel(repo=repo_name)
651 model = VcsSettingsModel(repo=repo_name)
651 delete_ui_patch = mock.patch.object(model.repo_settings, 'delete_ui')
652 delete_ui_patch = mock.patch.object(model.repo_settings, 'delete_ui')
652 with delete_ui_patch as delete_ui_mock:
653 with delete_ui_patch as delete_ui_mock:
653 model.delete_repo_svn_pattern(123)
654 model.delete_repo_svn_pattern(123)
654 Session().commit()
655 Session().commit()
655
656
656 delete_ui_mock.assert_called_once_with(-1)
657 delete_ui_mock.assert_called_once_with(-1)
657
658
658 def test_raises_exception_when_repository_is_not_specified(self):
659 def test_raises_exception_when_repository_is_not_specified(self):
659 model = VcsSettingsModel()
660 model = VcsSettingsModel()
660 with pytest.raises(Exception) as exc_info:
661 with pytest.raises(Exception) as exc_info:
661 model.delete_repo_svn_pattern(123)
662 model.delete_repo_svn_pattern(123)
662 assert str(exc_info.value) == 'Repository is not specified'
663 assert str(exc_info.value) == 'Repository is not specified'
663
664
664
665
665 class TestDeleteGlobalSvnPattern(object):
666 class TestDeleteGlobalSvnPattern(object):
666 def test_delete_global_svn_pattern_calls_delete_ui(self):
667 def test_delete_global_svn_pattern_calls_delete_ui(self):
667 model = VcsSettingsModel()
668 model = VcsSettingsModel()
668 delete_ui_patch = mock.patch.object(model.global_settings, 'delete_ui')
669 delete_ui_patch = mock.patch.object(model.global_settings, 'delete_ui')
669 with delete_ui_patch as delete_ui_mock:
670 with delete_ui_patch as delete_ui_mock:
670 model.delete_global_svn_pattern(123)
671 model.delete_global_svn_pattern(123)
671 delete_ui_mock.assert_called_once_with(123)
672 delete_ui_mock.assert_called_once_with(123)
672
673
673
674
674 class TestFilterUiSettings(object):
675 class TestFilterUiSettings(object):
675 def test_settings_are_filtered(self):
676 def test_settings_are_filtered(self):
676 model = VcsSettingsModel()
677 model = VcsSettingsModel()
677 repo_settings = [
678 repo_settings = [
678 UiSetting('extensions', 'largefiles', '', True),
679 UiSetting('extensions', 'largefiles', '', True),
679 UiSetting('phases', 'publish', 'True', True),
680 UiSetting('phases', 'publish', 'True', True),
680 UiSetting('hooks', 'changegroup.repo_size', 'hook', True),
681 UiSetting('hooks', 'changegroup.repo_size', 'hook', True),
681 UiSetting('hooks', 'changegroup.push_logger', 'hook', True),
682 UiSetting('hooks', 'changegroup.push_logger', 'hook', True),
682 UiSetting('hooks', 'outgoing.pull_logger', 'hook', True),
683 UiSetting('hooks', 'outgoing.pull_logger', 'hook', True),
683 UiSetting(
684 UiSetting(
684 'vcs_svn_branch', '84223c972204fa545ca1b22dac7bef5b68d7442d',
685 'vcs_svn_branch', '84223c972204fa545ca1b22dac7bef5b68d7442d',
685 'test_branch', True),
686 'test_branch', True),
686 UiSetting(
687 UiSetting(
687 'vcs_svn_tag', '84229c972204fa545ca1b22dac7bef5b68d7442d',
688 'vcs_svn_tag', '84229c972204fa545ca1b22dac7bef5b68d7442d',
688 'test_tag', True),
689 'test_tag', True),
689 ]
690 ]
690 non_repo_settings = [
691 non_repo_settings = [
691 UiSetting('largefiles', 'usercache', '/example/largefiles-store', True),
692 UiSetting('largefiles', 'usercache', '/example/largefiles-store', True),
692 UiSetting('test', 'outgoing.pull_logger', 'hook', True),
693 UiSetting('test', 'outgoing.pull_logger', 'hook', True),
693 UiSetting('hooks', 'test2', 'hook', True),
694 UiSetting('hooks', 'test2', 'hook', True),
694 UiSetting(
695 UiSetting(
695 'vcs_svn_repo', '84229c972204fa545ca1b22dac7bef5b68d7442d',
696 'vcs_svn_repo', '84229c972204fa545ca1b22dac7bef5b68d7442d',
696 'test_tag', True),
697 'test_tag', True),
697 ]
698 ]
698 settings = repo_settings + non_repo_settings
699 settings = repo_settings + non_repo_settings
699 filtered_settings = model._filter_ui_settings(settings)
700 filtered_settings = model._filter_ui_settings(settings)
700 assert sorted(filtered_settings) == sorted(repo_settings)
701 assert sorted(filtered_settings) == sorted(repo_settings)
701
702
702
703
703 class TestFilterGeneralSettings(object):
704 class TestFilterGeneralSettings(object):
704 def test_settings_are_filtered(self):
705 def test_settings_are_filtered(self):
705 model = VcsSettingsModel()
706 model = VcsSettingsModel()
706 settings = {
707 settings = {
707 'rhodecode_abcde': 'value1',
708 'rhodecode_abcde': 'value1',
708 'rhodecode_vwxyz': 'value2',
709 'rhodecode_vwxyz': 'value2',
709 }
710 }
710 general_settings = {
711 general_settings = {
711 'rhodecode_{}'.format(key): 'value'
712 'rhodecode_{}'.format(key): 'value'
712 for key in VcsSettingsModel.GENERAL_SETTINGS
713 for key in VcsSettingsModel.GENERAL_SETTINGS
713 }
714 }
714 settings.update(general_settings)
715 settings.update(general_settings)
715
716
716 filtered_settings = model._filter_general_settings(general_settings)
717 filtered_settings = model._filter_general_settings(general_settings)
717 assert sorted(filtered_settings) == sorted(general_settings)
718 assert sorted(filtered_settings) == sorted(general_settings)
718
719
719
720
720 class TestGetRepoUiSettings(object):
721 class TestGetRepoUiSettings(object):
721 def test_global_uis_are_returned_when_no_repo_uis_found(
722 def test_global_uis_are_returned_when_no_repo_uis_found(
722 self, repo_stub):
723 self, repo_stub):
723 model = VcsSettingsModel(repo=repo_stub.repo_name)
724 model = VcsSettingsModel(repo=repo_stub.repo_name)
724 result = model.get_repo_ui_settings()
725 result = model.get_repo_ui_settings()
725 svn_sections = (
726 svn_sections = (
726 VcsSettingsModel.SVN_TAG_SECTION,
727 VcsSettingsModel.SVN_TAG_SECTION,
727 VcsSettingsModel.SVN_BRANCH_SECTION)
728 VcsSettingsModel.SVN_BRANCH_SECTION)
728 expected_result = [
729 expected_result = [
729 s for s in model.global_settings.get_ui()
730 s for s in model.global_settings.get_ui()
730 if s.section not in svn_sections]
731 if s.section not in svn_sections]
731 assert sorted(result) == sorted(expected_result)
732 assert sorted(result) == sorted(expected_result)
732
733
733 def test_repo_uis_are_overriding_global_uis(
734 def test_repo_uis_are_overriding_global_uis(
734 self, repo_stub, settings_util):
735 self, repo_stub, settings_util):
735 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
736 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
736 settings_util.create_repo_rhodecode_ui(
737 settings_util.create_repo_rhodecode_ui(
737 repo_stub, section, 'repo', key=key, active=False)
738 repo_stub, section, 'repo', key=key, active=False)
738 model = VcsSettingsModel(repo=repo_stub.repo_name)
739 model = VcsSettingsModel(repo=repo_stub.repo_name)
739 result = model.get_repo_ui_settings()
740 result = model.get_repo_ui_settings()
740 for setting in result:
741 for setting in result:
741 locator = (setting.section, setting.key)
742 locator = (setting.section, setting.key)
742 if locator in VcsSettingsModel.HOOKS_SETTINGS:
743 if locator in VcsSettingsModel.HOOKS_SETTINGS:
743 assert setting.value == 'repo'
744 assert setting.value == 'repo'
744
745
745 assert setting.active is False
746 assert setting.active is False
746
747
747 def test_global_svn_patterns_are_not_in_list(
748 def test_global_svn_patterns_are_not_in_list(
748 self, repo_stub, settings_util):
749 self, repo_stub, settings_util):
749 svn_sections = (
750 svn_sections = (
750 VcsSettingsModel.SVN_TAG_SECTION,
751 VcsSettingsModel.SVN_TAG_SECTION,
751 VcsSettingsModel.SVN_BRANCH_SECTION)
752 VcsSettingsModel.SVN_BRANCH_SECTION)
752 for section in svn_sections:
753 for section in svn_sections:
753 settings_util.create_rhodecode_ui(
754 settings_util.create_rhodecode_ui(
754 section, 'repo', key='deadbeef' + section, active=False)
755 section, 'repo', key='deadbeef' + section, active=False)
755 Session().commit()
756 Session().commit()
756
757
757 model = VcsSettingsModel(repo=repo_stub.repo_name)
758 model = VcsSettingsModel(repo=repo_stub.repo_name)
758 result = model.get_repo_ui_settings()
759 result = model.get_repo_ui_settings()
759 for setting in result:
760 for setting in result:
760 assert setting.section not in svn_sections
761 assert setting.section not in svn_sections
761
762
762 def test_repo_uis_filtered_by_section_are_returned(
763 def test_repo_uis_filtered_by_section_are_returned(
763 self, repo_stub, settings_util):
764 self, repo_stub, settings_util):
764 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
765 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
765 settings_util.create_repo_rhodecode_ui(
766 settings_util.create_repo_rhodecode_ui(
766 repo_stub, section, 'repo', key=key, active=False)
767 repo_stub, section, 'repo', key=key, active=False)
767 model = VcsSettingsModel(repo=repo_stub.repo_name)
768 model = VcsSettingsModel(repo=repo_stub.repo_name)
768 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
769 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
769 result = model.get_repo_ui_settings(section=section)
770 result = model.get_repo_ui_settings(section=section)
770 for setting in result:
771 for setting in result:
771 assert setting.section == section
772 assert setting.section == section
772
773
773 def test_repo_uis_filtered_by_key_are_returned(
774 def test_repo_uis_filtered_by_key_are_returned(
774 self, repo_stub, settings_util):
775 self, repo_stub, settings_util):
775 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
776 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
776 settings_util.create_repo_rhodecode_ui(
777 settings_util.create_repo_rhodecode_ui(
777 repo_stub, section, 'repo', key=key, active=False)
778 repo_stub, section, 'repo', key=key, active=False)
778 model = VcsSettingsModel(repo=repo_stub.repo_name)
779 model = VcsSettingsModel(repo=repo_stub.repo_name)
779 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
780 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
780 result = model.get_repo_ui_settings(key=key)
781 result = model.get_repo_ui_settings(key=key)
781 for setting in result:
782 for setting in result:
782 assert setting.key == key
783 assert setting.key == key
783
784
784 def test_raises_exception_when_repository_is_not_specified(self):
785 def test_raises_exception_when_repository_is_not_specified(self):
785 model = VcsSettingsModel()
786 model = VcsSettingsModel()
786 with pytest.raises(Exception) as exc_info:
787 with pytest.raises(Exception) as exc_info:
787 model.get_repo_ui_settings()
788 model.get_repo_ui_settings()
788 assert str(exc_info.value) == 'Repository is not specified'
789 assert str(exc_info.value) == 'Repository is not specified'
789
790
790
791
791 class TestGetRepoGeneralSettings(object):
792 class TestGetRepoGeneralSettings(object):
792 def test_global_settings_are_returned_when_no_repo_settings_found(
793 def test_global_settings_are_returned_when_no_repo_settings_found(
793 self, repo_stub):
794 self, repo_stub):
794 model = VcsSettingsModel(repo=repo_stub.repo_name)
795 model = VcsSettingsModel(repo=repo_stub.repo_name)
795 result = model.get_repo_general_settings()
796 result = model.get_repo_general_settings()
796 expected_result = model.global_settings.get_all_settings()
797 expected_result = model.global_settings.get_all_settings()
797 assert sorted(result) == sorted(expected_result)
798 assert sorted(result) == sorted(expected_result)
798
799
799 def test_repo_uis_are_overriding_global_uis(
800 def test_repo_uis_are_overriding_global_uis(
800 self, repo_stub, settings_util):
801 self, repo_stub, settings_util):
801 for key in VcsSettingsModel.GENERAL_SETTINGS:
802 for key in VcsSettingsModel.GENERAL_SETTINGS:
802 settings_util.create_repo_rhodecode_setting(
803 settings_util.create_repo_rhodecode_setting(
803 repo_stub, key, 'abcde', type_='unicode')
804 repo_stub, key, 'abcde', type_='unicode')
804 Session().commit()
805 Session().commit()
805
806
806 model = VcsSettingsModel(repo=repo_stub.repo_name)
807 model = VcsSettingsModel(repo=repo_stub.repo_name)
807 result = model.get_repo_ui_settings()
808 result = model.get_repo_ui_settings()
808 for key in result:
809 for key in result:
809 if key in VcsSettingsModel.GENERAL_SETTINGS:
810 if key in VcsSettingsModel.GENERAL_SETTINGS:
810 assert result[key] == 'abcde'
811 assert result[key] == 'abcde'
811
812
812 def test_raises_exception_when_repository_is_not_specified(self):
813 def test_raises_exception_when_repository_is_not_specified(self):
813 model = VcsSettingsModel()
814 model = VcsSettingsModel()
814 with pytest.raises(Exception) as exc_info:
815 with pytest.raises(Exception) as exc_info:
815 model.get_repo_general_settings()
816 model.get_repo_general_settings()
816 assert str(exc_info.value) == 'Repository is not specified'
817 assert str(exc_info.value) == 'Repository is not specified'
817
818
818
819
819 class TestGetGlobalGeneralSettings(object):
820 class TestGetGlobalGeneralSettings(object):
820 def test_global_settings_are_returned(self, repo_stub):
821 def test_global_settings_are_returned(self, repo_stub):
821 model = VcsSettingsModel()
822 model = VcsSettingsModel()
822 result = model.get_global_general_settings()
823 result = model.get_global_general_settings()
823 expected_result = model.global_settings.get_all_settings()
824 expected_result = model.global_settings.get_all_settings()
824 assert sorted(result) == sorted(expected_result)
825 assert sorted(result) == sorted(expected_result)
825
826
826 def test_repo_uis_are_not_overriding_global_uis(
827 def test_repo_uis_are_not_overriding_global_uis(
827 self, repo_stub, settings_util):
828 self, repo_stub, settings_util):
828 for key in VcsSettingsModel.GENERAL_SETTINGS:
829 for key in VcsSettingsModel.GENERAL_SETTINGS:
829 settings_util.create_repo_rhodecode_setting(
830 settings_util.create_repo_rhodecode_setting(
830 repo_stub, key, 'abcde', type_='unicode')
831 repo_stub, key, 'abcde', type_='unicode')
831 Session().commit()
832 Session().commit()
832
833
833 model = VcsSettingsModel(repo=repo_stub.repo_name)
834 model = VcsSettingsModel(repo=repo_stub.repo_name)
834 result = model.get_global_general_settings()
835 result = model.get_global_general_settings()
835 expected_result = model.global_settings.get_all_settings()
836 expected_result = model.global_settings.get_all_settings()
836 assert sorted(result) == sorted(expected_result)
837 assert sorted(result) == sorted(expected_result)
837
838
838
839
839 class TestGetGlobalUiSettings(object):
840 class TestGetGlobalUiSettings(object):
840 def test_global_uis_are_returned(self, repo_stub):
841 def test_global_uis_are_returned(self, repo_stub):
841 model = VcsSettingsModel()
842 model = VcsSettingsModel()
842 result = model.get_global_ui_settings()
843 result = model.get_global_ui_settings()
843 expected_result = model.global_settings.get_ui()
844 expected_result = model.global_settings.get_ui()
844 assert sorted(result) == sorted(expected_result)
845 assert sorted(result) == sorted(expected_result)
845
846
846 def test_repo_uis_are_not_overriding_global_uis(
847 def test_repo_uis_are_not_overriding_global_uis(
847 self, repo_stub, settings_util):
848 self, repo_stub, settings_util):
848 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
849 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
849 settings_util.create_repo_rhodecode_ui(
850 settings_util.create_repo_rhodecode_ui(
850 repo_stub, section, 'repo', key=key, active=False)
851 repo_stub, section, 'repo', key=key, active=False)
851 Session().commit()
852 Session().commit()
852
853
853 model = VcsSettingsModel(repo=repo_stub.repo_name)
854 model = VcsSettingsModel(repo=repo_stub.repo_name)
854 result = model.get_global_ui_settings()
855 result = model.get_global_ui_settings()
855 expected_result = model.global_settings.get_ui()
856 expected_result = model.global_settings.get_ui()
856 assert sorted(result) == sorted(expected_result)
857 assert sorted(result) == sorted(expected_result)
857
858
858 def test_ui_settings_filtered_by_section(
859 def test_ui_settings_filtered_by_section(
859 self, repo_stub, settings_util):
860 self, repo_stub, settings_util):
860 model = VcsSettingsModel(repo=repo_stub.repo_name)
861 model = VcsSettingsModel(repo=repo_stub.repo_name)
861 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
862 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
862 result = model.get_global_ui_settings(section=section)
863 result = model.get_global_ui_settings(section=section)
863 expected_result = model.global_settings.get_ui(section=section)
864 expected_result = model.global_settings.get_ui(section=section)
864 assert sorted(result) == sorted(expected_result)
865 assert sorted(result) == sorted(expected_result)
865
866
866 def test_ui_settings_filtered_by_key(
867 def test_ui_settings_filtered_by_key(
867 self, repo_stub, settings_util):
868 self, repo_stub, settings_util):
868 model = VcsSettingsModel(repo=repo_stub.repo_name)
869 model = VcsSettingsModel(repo=repo_stub.repo_name)
869 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
870 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
870 result = model.get_global_ui_settings(key=key)
871 result = model.get_global_ui_settings(key=key)
871 expected_result = model.global_settings.get_ui(key=key)
872 expected_result = model.global_settings.get_ui(key=key)
872 assert sorted(result) == sorted(expected_result)
873 assert sorted(result) == sorted(expected_result)
873
874
874
875
875 class TestGetGeneralSettings(object):
876 class TestGetGeneralSettings(object):
876 def test_global_settings_are_returned_when_inherited_is_true(
877 def test_global_settings_are_returned_when_inherited_is_true(
877 self, repo_stub, settings_util):
878 self, repo_stub, settings_util):
878 model = VcsSettingsModel(repo=repo_stub.repo_name)
879 model = VcsSettingsModel(repo=repo_stub.repo_name)
879 model.inherit_global_settings = True
880 model.inherit_global_settings = True
880 for key in VcsSettingsModel.GENERAL_SETTINGS:
881 for key in VcsSettingsModel.GENERAL_SETTINGS:
881 settings_util.create_repo_rhodecode_setting(
882 settings_util.create_repo_rhodecode_setting(
882 repo_stub, key, 'abcde', type_='unicode')
883 repo_stub, key, 'abcde', type_='unicode')
883 Session().commit()
884 Session().commit()
884
885
885 result = model.get_general_settings()
886 result = model.get_general_settings()
886 expected_result = model.get_global_general_settings()
887 expected_result = model.get_global_general_settings()
887 assert sorted(result) == sorted(expected_result)
888 assert sorted(result) == sorted(expected_result)
888
889
889 def test_repo_settings_are_returned_when_inherited_is_false(
890 def test_repo_settings_are_returned_when_inherited_is_false(
890 self, repo_stub, settings_util):
891 self, repo_stub, settings_util):
891 model = VcsSettingsModel(repo=repo_stub.repo_name)
892 model = VcsSettingsModel(repo=repo_stub.repo_name)
892 model.inherit_global_settings = False
893 model.inherit_global_settings = False
893 for key in VcsSettingsModel.GENERAL_SETTINGS:
894 for key in VcsSettingsModel.GENERAL_SETTINGS:
894 settings_util.create_repo_rhodecode_setting(
895 settings_util.create_repo_rhodecode_setting(
895 repo_stub, key, 'abcde', type_='unicode')
896 repo_stub, key, 'abcde', type_='unicode')
896 Session().commit()
897 Session().commit()
897
898
898 result = model.get_general_settings()
899 result = model.get_general_settings()
899 expected_result = model.get_repo_general_settings()
900 expected_result = model.get_repo_general_settings()
900 assert sorted(result) == sorted(expected_result)
901 assert sorted(result) == sorted(expected_result)
901
902
902 def test_global_settings_are_returned_when_no_repository_specified(self):
903 def test_global_settings_are_returned_when_no_repository_specified(self):
903 model = VcsSettingsModel()
904 model = VcsSettingsModel()
904 result = model.get_general_settings()
905 result = model.get_general_settings()
905 expected_result = model.get_global_general_settings()
906 expected_result = model.get_global_general_settings()
906 assert sorted(result) == sorted(expected_result)
907 assert sorted(result) == sorted(expected_result)
907
908
908
909
909 class TestGetUiSettings(object):
910 class TestGetUiSettings(object):
910 def test_global_settings_are_returned_when_inherited_is_true(
911 def test_global_settings_are_returned_when_inherited_is_true(
911 self, repo_stub, settings_util):
912 self, repo_stub, settings_util):
912 model = VcsSettingsModel(repo=repo_stub.repo_name)
913 model = VcsSettingsModel(repo=repo_stub.repo_name)
913 model.inherit_global_settings = True
914 model.inherit_global_settings = True
914 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
915 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
915 settings_util.create_repo_rhodecode_ui(
916 settings_util.create_repo_rhodecode_ui(
916 repo_stub, section, 'repo', key=key, active=True)
917 repo_stub, section, 'repo', key=key, active=True)
917 Session().commit()
918 Session().commit()
918
919
919 result = model.get_ui_settings()
920 result = model.get_ui_settings()
920 expected_result = model.get_global_ui_settings()
921 expected_result = model.get_global_ui_settings()
921 assert sorted(result) == sorted(expected_result)
922 assert sorted(result) == sorted(expected_result)
922
923
923 def test_repo_settings_are_returned_when_inherited_is_false(
924 def test_repo_settings_are_returned_when_inherited_is_false(
924 self, repo_stub, settings_util):
925 self, repo_stub, settings_util):
925 model = VcsSettingsModel(repo=repo_stub.repo_name)
926 model = VcsSettingsModel(repo=repo_stub.repo_name)
926 model.inherit_global_settings = False
927 model.inherit_global_settings = False
927 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
928 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
928 settings_util.create_repo_rhodecode_ui(
929 settings_util.create_repo_rhodecode_ui(
929 repo_stub, section, 'repo', key=key, active=True)
930 repo_stub, section, 'repo', key=key, active=True)
930 Session().commit()
931 Session().commit()
931
932
932 result = model.get_ui_settings()
933 result = model.get_ui_settings()
933 expected_result = model.get_repo_ui_settings()
934 expected_result = model.get_repo_ui_settings()
934 assert sorted(result) == sorted(expected_result)
935 assert sorted(result) == sorted(expected_result)
935
936
936 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
937 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
937 model = VcsSettingsModel(repo=repo_stub.repo_name)
938 model = VcsSettingsModel(repo=repo_stub.repo_name)
938 model.inherit_global_settings = False
939 model.inherit_global_settings = False
939
940
940 args = ('section', 'key')
941 args = ('section', 'key')
941 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
942 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
942 model.get_ui_settings(*args)
943 model.get_ui_settings(*args)
943 Session().commit()
944 Session().commit()
944
945
945 settings_mock.assert_called_once_with(*args)
946 settings_mock.assert_called_once_with(*args)
946
947
947 def test_global_settings_filtered_by_section_and_key(self):
948 def test_global_settings_filtered_by_section_and_key(self):
948 model = VcsSettingsModel()
949 model = VcsSettingsModel()
949 args = ('section', 'key')
950 args = ('section', 'key')
950 with mock.patch.object(model, 'get_global_ui_settings') as (
951 with mock.patch.object(model, 'get_global_ui_settings') as (
951 settings_mock):
952 settings_mock):
952 model.get_ui_settings(*args)
953 model.get_ui_settings(*args)
953 settings_mock.assert_called_once_with(*args)
954 settings_mock.assert_called_once_with(*args)
954
955
955 def test_global_settings_are_returned_when_no_repository_specified(self):
956 def test_global_settings_are_returned_when_no_repository_specified(self):
956 model = VcsSettingsModel()
957 model = VcsSettingsModel()
957 result = model.get_ui_settings()
958 result = model.get_ui_settings()
958 expected_result = model.get_global_ui_settings()
959 expected_result = model.get_global_ui_settings()
959 assert sorted(result) == sorted(expected_result)
960 assert sorted(result) == sorted(expected_result)
960
961
961
962
962 class TestGetSvnPatterns(object):
963 class TestGetSvnPatterns(object):
963 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
964 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
964 model = VcsSettingsModel(repo=repo_stub.repo_name)
965 model = VcsSettingsModel(repo=repo_stub.repo_name)
965 args = ('section', )
966 args = ('section', )
966 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
967 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
967 model.get_svn_patterns(*args)
968 model.get_svn_patterns(*args)
968
969
969 Session().commit()
970 Session().commit()
970 settings_mock.assert_called_once_with(*args)
971 settings_mock.assert_called_once_with(*args)
971
972
972 def test_global_settings_filtered_by_section_and_key(self):
973 def test_global_settings_filtered_by_section_and_key(self):
973 model = VcsSettingsModel()
974 model = VcsSettingsModel()
974 args = ('section', )
975 args = ('section', )
975 with mock.patch.object(model, 'get_global_ui_settings') as (
976 with mock.patch.object(model, 'get_global_ui_settings') as (
976 settings_mock):
977 settings_mock):
977 model.get_svn_patterns(*args)
978 model.get_svn_patterns(*args)
978 settings_mock.assert_called_once_with(*args)
979 settings_mock.assert_called_once_with(*args)
979
980
980
981
981 class TestCreateOrUpdateRepoSettings(object):
982 class TestCreateOrUpdateRepoSettings(object):
982 FORM_DATA = {
983 FORM_DATA = {
983 'inherit_global_settings': False,
984 'inherit_global_settings': False,
984 'hooks_changegroup_repo_size': False,
985 'hooks_changegroup_repo_size': False,
985 'hooks_changegroup_push_logger': False,
986 'hooks_changegroup_push_logger': False,
986 'hooks_outgoing_pull_logger': False,
987 'hooks_outgoing_pull_logger': False,
987 'extensions_largefiles': False,
988 'extensions_largefiles': False,
988 'extensions_evolve': False,
989 'extensions_evolve': False,
989 'vcs_git_lfs_enabled': False,
990 'vcs_git_lfs_enabled': False,
990 'phases_publish': 'False',
991 'phases_publish': 'False',
991 'rhodecode_pr_merge_enabled': False,
992 'rhodecode_pr_merge_enabled': False,
993 'rhodecode_auto_merge_enabled': False,
992 'rhodecode_use_outdated_comments': False,
994 'rhodecode_use_outdated_comments': False,
993 'new_svn_branch': '',
995 'new_svn_branch': '',
994 'new_svn_tag': ''
996 'new_svn_tag': ''
995 }
997 }
996
998
997 def test_get_raises_exception_when_repository_not_specified(self):
999 def test_get_raises_exception_when_repository_not_specified(self):
998 model = VcsSettingsModel()
1000 model = VcsSettingsModel()
999 with pytest.raises(Exception) as exc_info:
1001 with pytest.raises(Exception) as exc_info:
1000 model.create_or_update_repo_settings(data=self.FORM_DATA)
1002 model.create_or_update_repo_settings(data=self.FORM_DATA)
1001 Session().commit()
1003 Session().commit()
1002
1004
1003 assert str(exc_info.value) == 'Repository is not specified'
1005 assert str(exc_info.value) == 'Repository is not specified'
1004
1006
1005 def test_only_svn_settings_are_updated_when_type_is_svn(self, backend_svn):
1007 def test_only_svn_settings_are_updated_when_type_is_svn(self, backend_svn):
1006 repo = backend_svn.create_repo()
1008 repo = backend_svn.create_repo()
1007 model = VcsSettingsModel(repo=repo)
1009 model = VcsSettingsModel(repo=repo)
1008 with self._patch_model(model) as mocks:
1010 with self._patch_model(model) as mocks:
1009 model.create_or_update_repo_settings(
1011 model.create_or_update_repo_settings(
1010 data=self.FORM_DATA, inherit_global_settings=False)
1012 data=self.FORM_DATA, inherit_global_settings=False)
1011 Session().commit()
1013 Session().commit()
1012
1014
1013 mocks['create_repo_svn_settings'].assert_called_once_with(
1015 mocks['create_repo_svn_settings'].assert_called_once_with(
1014 self.FORM_DATA)
1016 self.FORM_DATA)
1015 non_called_methods = (
1017 non_called_methods = (
1016 'create_or_update_repo_hook_settings',
1018 'create_or_update_repo_hook_settings',
1017 'create_or_update_repo_pr_settings',
1019 'create_or_update_repo_pr_settings',
1018 'create_or_update_repo_hg_settings')
1020 'create_or_update_repo_hg_settings')
1019 for method in non_called_methods:
1021 for method in non_called_methods:
1020 assert mocks[method].call_count == 0
1022 assert mocks[method].call_count == 0
1021
1023
1022 def test_non_svn_settings_are_updated_when_type_is_hg(self, backend_hg):
1024 def test_non_svn_settings_are_updated_when_type_is_hg(self, backend_hg):
1023 repo = backend_hg.create_repo()
1025 repo = backend_hg.create_repo()
1024 model = VcsSettingsModel(repo=repo)
1026 model = VcsSettingsModel(repo=repo)
1025 with self._patch_model(model) as mocks:
1027 with self._patch_model(model) as mocks:
1026 model.create_or_update_repo_settings(
1028 model.create_or_update_repo_settings(
1027 data=self.FORM_DATA, inherit_global_settings=False)
1029 data=self.FORM_DATA, inherit_global_settings=False)
1028 Session().commit()
1030 Session().commit()
1029
1031
1030 assert mocks['create_repo_svn_settings'].call_count == 0
1032 assert mocks['create_repo_svn_settings'].call_count == 0
1031 called_methods = (
1033 called_methods = (
1032 'create_or_update_repo_hook_settings',
1034 'create_or_update_repo_hook_settings',
1033 'create_or_update_repo_pr_settings',
1035 'create_or_update_repo_pr_settings',
1034 'create_or_update_repo_hg_settings')
1036 'create_or_update_repo_hg_settings')
1035 for method in called_methods:
1037 for method in called_methods:
1036 mocks[method].assert_called_once_with(self.FORM_DATA)
1038 mocks[method].assert_called_once_with(self.FORM_DATA)
1037
1039
1038 def test_non_svn_and_hg_settings_are_updated_when_type_is_git(
1040 def test_non_svn_and_hg_settings_are_updated_when_type_is_git(
1039 self, backend_git):
1041 self, backend_git):
1040 repo = backend_git.create_repo()
1042 repo = backend_git.create_repo()
1041 model = VcsSettingsModel(repo=repo)
1043 model = VcsSettingsModel(repo=repo)
1042 with self._patch_model(model) as mocks:
1044 with self._patch_model(model) as mocks:
1043 model.create_or_update_repo_settings(
1045 model.create_or_update_repo_settings(
1044 data=self.FORM_DATA, inherit_global_settings=False)
1046 data=self.FORM_DATA, inherit_global_settings=False)
1045
1047
1046 assert mocks['create_repo_svn_settings'].call_count == 0
1048 assert mocks['create_repo_svn_settings'].call_count == 0
1047 called_methods = (
1049 called_methods = (
1048 'create_or_update_repo_hook_settings',
1050 'create_or_update_repo_hook_settings',
1049 'create_or_update_repo_pr_settings')
1051 'create_or_update_repo_pr_settings')
1050 non_called_methods = (
1052 non_called_methods = (
1051 'create_repo_svn_settings',
1053 'create_repo_svn_settings',
1052 'create_or_update_repo_hg_settings'
1054 'create_or_update_repo_hg_settings'
1053 )
1055 )
1054 for method in called_methods:
1056 for method in called_methods:
1055 mocks[method].assert_called_once_with(self.FORM_DATA)
1057 mocks[method].assert_called_once_with(self.FORM_DATA)
1056 for method in non_called_methods:
1058 for method in non_called_methods:
1057 assert mocks[method].call_count == 0
1059 assert mocks[method].call_count == 0
1058
1060
1059 def test_no_methods_are_called_when_settings_are_inherited(
1061 def test_no_methods_are_called_when_settings_are_inherited(
1060 self, backend):
1062 self, backend):
1061 repo = backend.create_repo()
1063 repo = backend.create_repo()
1062 model = VcsSettingsModel(repo=repo)
1064 model = VcsSettingsModel(repo=repo)
1063 with self._patch_model(model) as mocks:
1065 with self._patch_model(model) as mocks:
1064 model.create_or_update_repo_settings(
1066 model.create_or_update_repo_settings(
1065 data=self.FORM_DATA, inherit_global_settings=True)
1067 data=self.FORM_DATA, inherit_global_settings=True)
1066 for method_name in mocks:
1068 for method_name in mocks:
1067 assert mocks[method_name].call_count == 0
1069 assert mocks[method_name].call_count == 0
1068
1070
1069 def test_cache_is_marked_for_invalidation(self, repo_stub):
1071 def test_cache_is_marked_for_invalidation(self, repo_stub):
1070 model = VcsSettingsModel(repo=repo_stub)
1072 model = VcsSettingsModel(repo=repo_stub)
1071 invalidation_patcher = mock.patch(
1073 invalidation_patcher = mock.patch(
1072 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
1074 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
1073 with invalidation_patcher as invalidation_mock:
1075 with invalidation_patcher as invalidation_mock:
1074 model.create_or_update_repo_settings(
1076 model.create_or_update_repo_settings(
1075 data=self.FORM_DATA, inherit_global_settings=True)
1077 data=self.FORM_DATA, inherit_global_settings=True)
1076 Session().commit()
1078 Session().commit()
1077
1079
1078 invalidation_mock.assert_called_once_with(
1080 invalidation_mock.assert_called_once_with(
1079 repo_stub.repo_name, delete=True)
1081 repo_stub.repo_name, delete=True)
1080
1082
1081 def test_inherit_flag_is_saved(self, repo_stub):
1083 def test_inherit_flag_is_saved(self, repo_stub):
1082 model = VcsSettingsModel(repo=repo_stub)
1084 model = VcsSettingsModel(repo=repo_stub)
1083 model.inherit_global_settings = True
1085 model.inherit_global_settings = True
1084 with self._patch_model(model):
1086 with self._patch_model(model):
1085 model.create_or_update_repo_settings(
1087 model.create_or_update_repo_settings(
1086 data=self.FORM_DATA, inherit_global_settings=False)
1088 data=self.FORM_DATA, inherit_global_settings=False)
1087 Session().commit()
1089 Session().commit()
1088
1090
1089 assert model.inherit_global_settings is False
1091 assert model.inherit_global_settings is False
1090
1092
1091 def _patch_model(self, model):
1093 def _patch_model(self, model):
1092 return mock.patch.multiple(
1094 return mock.patch.multiple(
1093 model,
1095 model,
1094 create_repo_svn_settings=mock.DEFAULT,
1096 create_repo_svn_settings=mock.DEFAULT,
1095 create_or_update_repo_hook_settings=mock.DEFAULT,
1097 create_or_update_repo_hook_settings=mock.DEFAULT,
1096 create_or_update_repo_pr_settings=mock.DEFAULT,
1098 create_or_update_repo_pr_settings=mock.DEFAULT,
1097 create_or_update_repo_hg_settings=mock.DEFAULT)
1099 create_or_update_repo_hg_settings=mock.DEFAULT)
General Comments 0
You need to be logged in to leave comments. Login now