##// END OF EJS Templates
fix(mercurial): dropped support for depreacted hgsubversion fixes RCCE-12
super-admin -
r5250:c24343a3 default
parent child Browse files
Show More
@@ -1,695 +1,678 b''
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 import rhodecode
23 import rhodecode
24 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.lib.hash_utils import md5_safe
25 from rhodecode.lib.hash_utils import md5_safe
26 from rhodecode.model.db import RhodeCodeUi
26 from rhodecode.model.db import RhodeCodeUi
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
28 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
29 from rhodecode.tests import assert_session_flash
29 from rhodecode.tests import assert_session_flash
30 from rhodecode.tests.routes import route_path
30 from rhodecode.tests.routes import route_path
31
31
32
32
33 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
33 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
34
34
35
35
36 @pytest.mark.usefixtures('autologin_user', 'app')
36 @pytest.mark.usefixtures('autologin_user', 'app')
37 class TestAdminSettingsController(object):
37 class TestAdminSettingsController(object):
38
38
39 @pytest.mark.parametrize('urlname', [
39 @pytest.mark.parametrize('urlname', [
40 'admin_settings_vcs',
40 'admin_settings_vcs',
41 'admin_settings_mapping',
41 'admin_settings_mapping',
42 'admin_settings_global',
42 'admin_settings_global',
43 'admin_settings_visual',
43 'admin_settings_visual',
44 'admin_settings_email',
44 'admin_settings_email',
45 'admin_settings_hooks',
45 'admin_settings_hooks',
46 'admin_settings_search',
46 'admin_settings_search',
47 ])
47 ])
48 def test_simple_get(self, urlname):
48 def test_simple_get(self, urlname):
49 self.app.get(route_path(urlname))
49 self.app.get(route_path(urlname))
50
50
51 def test_create_custom_hook(self, csrf_token):
51 def test_create_custom_hook(self, csrf_token):
52 response = self.app.post(
52 response = self.app.post(
53 route_path('admin_settings_hooks_update'),
53 route_path('admin_settings_hooks_update'),
54 params={
54 params={
55 'new_hook_ui_key': 'test_hooks_1',
55 'new_hook_ui_key': 'test_hooks_1',
56 'new_hook_ui_value': 'cd /tmp',
56 'new_hook_ui_value': 'cd /tmp',
57 'csrf_token': csrf_token})
57 'csrf_token': csrf_token})
58
58
59 response = response.follow()
59 response = response.follow()
60 response.mustcontain('test_hooks_1')
60 response.mustcontain('test_hooks_1')
61 response.mustcontain('cd /tmp')
61 response.mustcontain('cd /tmp')
62
62
63 def test_create_custom_hook_delete(self, csrf_token):
63 def test_create_custom_hook_delete(self, csrf_token):
64 response = self.app.post(
64 response = self.app.post(
65 route_path('admin_settings_hooks_update'),
65 route_path('admin_settings_hooks_update'),
66 params={
66 params={
67 'new_hook_ui_key': 'test_hooks_2',
67 'new_hook_ui_key': 'test_hooks_2',
68 'new_hook_ui_value': 'cd /tmp2',
68 'new_hook_ui_value': 'cd /tmp2',
69 'csrf_token': csrf_token})
69 'csrf_token': csrf_token})
70
70
71 response = response.follow()
71 response = response.follow()
72 response.mustcontain('test_hooks_2')
72 response.mustcontain('test_hooks_2')
73 response.mustcontain('cd /tmp2')
73 response.mustcontain('cd /tmp2')
74
74
75 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
75 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
76
76
77 # delete
77 # delete
78 self.app.post(
78 self.app.post(
79 route_path('admin_settings_hooks_delete'),
79 route_path('admin_settings_hooks_delete'),
80 params={'hook_id': hook_id, 'csrf_token': csrf_token})
80 params={'hook_id': hook_id, 'csrf_token': csrf_token})
81 response = self.app.get(route_path('admin_settings_hooks'))
81 response = self.app.get(route_path('admin_settings_hooks'))
82 response.mustcontain(no=['test_hooks_2'])
82 response.mustcontain(no=['test_hooks_2'])
83 response.mustcontain(no=['cd /tmp2'])
83 response.mustcontain(no=['cd /tmp2'])
84
84
85
85
86 @pytest.mark.usefixtures('autologin_user', 'app')
86 @pytest.mark.usefixtures('autologin_user', 'app')
87 class TestAdminSettingsGlobal(object):
87 class TestAdminSettingsGlobal(object):
88
88
89 def test_pre_post_code_code_active(self, csrf_token):
89 def test_pre_post_code_code_active(self, csrf_token):
90 pre_code = 'rc-pre-code-187652122'
90 pre_code = 'rc-pre-code-187652122'
91 post_code = 'rc-postcode-98165231'
91 post_code = 'rc-postcode-98165231'
92
92
93 response = self.post_and_verify_settings({
93 response = self.post_and_verify_settings({
94 'rhodecode_pre_code': pre_code,
94 'rhodecode_pre_code': pre_code,
95 'rhodecode_post_code': post_code,
95 'rhodecode_post_code': post_code,
96 'csrf_token': csrf_token,
96 'csrf_token': csrf_token,
97 })
97 })
98
98
99 response = response.follow()
99 response = response.follow()
100 response.mustcontain(pre_code, post_code)
100 response.mustcontain(pre_code, post_code)
101
101
102 def test_pre_post_code_code_inactive(self, csrf_token):
102 def test_pre_post_code_code_inactive(self, csrf_token):
103 pre_code = 'rc-pre-code-187652122'
103 pre_code = 'rc-pre-code-187652122'
104 post_code = 'rc-postcode-98165231'
104 post_code = 'rc-postcode-98165231'
105 response = self.post_and_verify_settings({
105 response = self.post_and_verify_settings({
106 'rhodecode_pre_code': '',
106 'rhodecode_pre_code': '',
107 'rhodecode_post_code': '',
107 'rhodecode_post_code': '',
108 'csrf_token': csrf_token,
108 'csrf_token': csrf_token,
109 })
109 })
110
110
111 response = response.follow()
111 response = response.follow()
112 response.mustcontain(no=[pre_code, post_code])
112 response.mustcontain(no=[pre_code, post_code])
113
113
114 def test_captcha_activate(self, csrf_token):
114 def test_captcha_activate(self, csrf_token):
115 self.post_and_verify_settings({
115 self.post_and_verify_settings({
116 'rhodecode_captcha_private_key': '1234567890',
116 'rhodecode_captcha_private_key': '1234567890',
117 'rhodecode_captcha_public_key': '1234567890',
117 'rhodecode_captcha_public_key': '1234567890',
118 'csrf_token': csrf_token,
118 'csrf_token': csrf_token,
119 })
119 })
120
120
121 response = self.app.get(ADMIN_PREFIX + '/register')
121 response = self.app.get(ADMIN_PREFIX + '/register')
122 response.mustcontain('captcha')
122 response.mustcontain('captcha')
123
123
124 def test_captcha_deactivate(self, csrf_token):
124 def test_captcha_deactivate(self, csrf_token):
125 self.post_and_verify_settings({
125 self.post_and_verify_settings({
126 'rhodecode_captcha_private_key': '',
126 'rhodecode_captcha_private_key': '',
127 'rhodecode_captcha_public_key': '1234567890',
127 'rhodecode_captcha_public_key': '1234567890',
128 'csrf_token': csrf_token,
128 'csrf_token': csrf_token,
129 })
129 })
130
130
131 response = self.app.get(ADMIN_PREFIX + '/register')
131 response = self.app.get(ADMIN_PREFIX + '/register')
132 response.mustcontain(no=['captcha'])
132 response.mustcontain(no=['captcha'])
133
133
134 def test_title_change(self, csrf_token):
134 def test_title_change(self, csrf_token):
135 old_title = 'RhodeCode'
135 old_title = 'RhodeCode'
136
136
137 for new_title in ['Changed', 'Ε»Γ³Ε‚wik', old_title]:
137 for new_title in ['Changed', 'Ε»Γ³Ε‚wik', old_title]:
138 response = self.post_and_verify_settings({
138 response = self.post_and_verify_settings({
139 'rhodecode_title': new_title,
139 'rhodecode_title': new_title,
140 'csrf_token': csrf_token,
140 'csrf_token': csrf_token,
141 })
141 })
142
142
143 response = response.follow()
143 response = response.follow()
144 response.mustcontain(new_title)
144 response.mustcontain(new_title)
145
145
146 def post_and_verify_settings(self, settings):
146 def post_and_verify_settings(self, settings):
147 old_title = 'RhodeCode'
147 old_title = 'RhodeCode'
148 old_realm = 'RhodeCode authentication'
148 old_realm = 'RhodeCode authentication'
149 params = {
149 params = {
150 'rhodecode_title': old_title,
150 'rhodecode_title': old_title,
151 'rhodecode_realm': old_realm,
151 'rhodecode_realm': old_realm,
152 'rhodecode_pre_code': '',
152 'rhodecode_pre_code': '',
153 'rhodecode_post_code': '',
153 'rhodecode_post_code': '',
154 'rhodecode_captcha_private_key': '',
154 'rhodecode_captcha_private_key': '',
155 'rhodecode_captcha_public_key': '',
155 'rhodecode_captcha_public_key': '',
156 'rhodecode_create_personal_repo_group': False,
156 'rhodecode_create_personal_repo_group': False,
157 'rhodecode_personal_repo_group_pattern': '${username}',
157 'rhodecode_personal_repo_group_pattern': '${username}',
158 }
158 }
159 params.update(settings)
159 params.update(settings)
160 response = self.app.post(
160 response = self.app.post(
161 route_path('admin_settings_global_update'), params=params)
161 route_path('admin_settings_global_update'), params=params)
162
162
163 assert_session_flash(response, 'Updated application settings')
163 assert_session_flash(response, 'Updated application settings')
164
164
165 app_settings = SettingsModel().get_all_settings()
165 app_settings = SettingsModel().get_all_settings()
166 del settings['csrf_token']
166 del settings['csrf_token']
167 for key, value in settings.items():
167 for key, value in settings.items():
168 assert app_settings[key] == value
168 assert app_settings[key] == value
169
169
170 return response
170 return response
171
171
172
172
173 @pytest.mark.usefixtures('autologin_user', 'app')
173 @pytest.mark.usefixtures('autologin_user', 'app')
174 class TestAdminSettingsVcs(object):
174 class TestAdminSettingsVcs(object):
175
175
176 def test_contains_svn_default_patterns(self):
176 def test_contains_svn_default_patterns(self):
177 response = self.app.get(route_path('admin_settings_vcs'))
177 response = self.app.get(route_path('admin_settings_vcs'))
178 expected_patterns = [
178 expected_patterns = [
179 '/trunk',
179 '/trunk',
180 '/branches/*',
180 '/branches/*',
181 '/tags/*',
181 '/tags/*',
182 ]
182 ]
183 for pattern in expected_patterns:
183 for pattern in expected_patterns:
184 response.mustcontain(pattern)
184 response.mustcontain(pattern)
185
185
186 def test_add_new_svn_branch_and_tag_pattern(
186 def test_add_new_svn_branch_and_tag_pattern(
187 self, backend_svn, form_defaults, disable_sql_cache,
187 self, backend_svn, form_defaults, disable_sql_cache,
188 csrf_token):
188 csrf_token):
189 form_defaults.update({
189 form_defaults.update({
190 'new_svn_branch': '/exp/branches/*',
190 'new_svn_branch': '/exp/branches/*',
191 'new_svn_tag': '/important_tags/*',
191 'new_svn_tag': '/important_tags/*',
192 'csrf_token': csrf_token,
192 'csrf_token': csrf_token,
193 })
193 })
194
194
195 response = self.app.post(
195 response = self.app.post(
196 route_path('admin_settings_vcs_update'),
196 route_path('admin_settings_vcs_update'),
197 params=form_defaults, status=302)
197 params=form_defaults, status=302)
198 response = response.follow()
198 response = response.follow()
199
199
200 # Expect to find the new values on the page
200 # Expect to find the new values on the page
201 response.mustcontain('/exp/branches/*')
201 response.mustcontain('/exp/branches/*')
202 response.mustcontain('/important_tags/*')
202 response.mustcontain('/important_tags/*')
203
203
204 # Expect that those patterns are used to match branches and tags now
204 # Expect that those patterns are used to match branches and tags now
205 repo = backend_svn['svn-simple-layout'].scm_instance()
205 repo = backend_svn['svn-simple-layout'].scm_instance()
206 assert 'exp/branches/exp-sphinx-docs' in repo.branches
206 assert 'exp/branches/exp-sphinx-docs' in repo.branches
207 assert 'important_tags/v0.5' in repo.tags
207 assert 'important_tags/v0.5' in repo.tags
208
208
209 def test_add_same_svn_value_twice_shows_an_error_message(
209 def test_add_same_svn_value_twice_shows_an_error_message(
210 self, form_defaults, csrf_token, settings_util):
210 self, form_defaults, csrf_token, settings_util):
211 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
211 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
212 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
212 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
213
213
214 response = self.app.post(
214 response = self.app.post(
215 route_path('admin_settings_vcs_update'),
215 route_path('admin_settings_vcs_update'),
216 params={
216 params={
217 'paths_root_path': form_defaults['paths_root_path'],
217 'paths_root_path': form_defaults['paths_root_path'],
218 'new_svn_branch': '/test',
218 'new_svn_branch': '/test',
219 'new_svn_tag': '/test',
219 'new_svn_tag': '/test',
220 'csrf_token': csrf_token,
220 'csrf_token': csrf_token,
221 },
221 },
222 status=200)
222 status=200)
223
223
224 response.mustcontain("Pattern already exists")
224 response.mustcontain("Pattern already exists")
225 response.mustcontain("Some form inputs contain invalid data.")
225 response.mustcontain("Some form inputs contain invalid data.")
226
226
227 @pytest.mark.parametrize('section', [
227 @pytest.mark.parametrize('section', [
228 'vcs_svn_branch',
228 'vcs_svn_branch',
229 'vcs_svn_tag',
229 'vcs_svn_tag',
230 ])
230 ])
231 def test_delete_svn_patterns(
231 def test_delete_svn_patterns(
232 self, section, csrf_token, settings_util):
232 self, section, csrf_token, settings_util):
233 setting = settings_util.create_rhodecode_ui(
233 setting = settings_util.create_rhodecode_ui(
234 section, '/test_delete', cleanup=False)
234 section, '/test_delete', cleanup=False)
235
235
236 self.app.post(
236 self.app.post(
237 route_path('admin_settings_vcs_svn_pattern_delete'),
237 route_path('admin_settings_vcs_svn_pattern_delete'),
238 params={
238 params={
239 'delete_svn_pattern': setting.ui_id,
239 'delete_svn_pattern': setting.ui_id,
240 'csrf_token': csrf_token},
240 'csrf_token': csrf_token},
241 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
241 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
242
242
243 @pytest.mark.parametrize('section', [
243 @pytest.mark.parametrize('section', [
244 'vcs_svn_branch',
244 'vcs_svn_branch',
245 'vcs_svn_tag',
245 'vcs_svn_tag',
246 ])
246 ])
247 def test_delete_svn_patterns_raises_404_when_no_xhr(
247 def test_delete_svn_patterns_raises_404_when_no_xhr(
248 self, section, csrf_token, settings_util):
248 self, section, csrf_token, settings_util):
249 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
249 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
250
250
251 self.app.post(
251 self.app.post(
252 route_path('admin_settings_vcs_svn_pattern_delete'),
252 route_path('admin_settings_vcs_svn_pattern_delete'),
253 params={
253 params={
254 'delete_svn_pattern': setting.ui_id,
254 'delete_svn_pattern': setting.ui_id,
255 'csrf_token': csrf_token},
255 'csrf_token': csrf_token},
256 status=404)
256 status=404)
257
257
258 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
259 form_defaults.update({
260 'csrf_token': csrf_token,
261 'extensions_hgsubversion': 'True',
262 })
263 response = self.app.post(
264 route_path('admin_settings_vcs_update'),
265 params=form_defaults,
266 status=302)
267
268 response = response.follow()
269 extensions_input = (
270 '<input id="extensions_hgsubversion" '
271 'name="extensions_hgsubversion" type="checkbox" '
272 'value="True" checked="checked" />')
273 response.mustcontain(extensions_input)
274
275 def test_extensions_hgevolve(self, form_defaults, csrf_token):
258 def test_extensions_hgevolve(self, form_defaults, csrf_token):
276 form_defaults.update({
259 form_defaults.update({
277 'csrf_token': csrf_token,
260 'csrf_token': csrf_token,
278 'extensions_evolve': 'True',
261 'extensions_evolve': 'True',
279 })
262 })
280 response = self.app.post(
263 response = self.app.post(
281 route_path('admin_settings_vcs_update'),
264 route_path('admin_settings_vcs_update'),
282 params=form_defaults,
265 params=form_defaults,
283 status=302)
266 status=302)
284
267
285 response = response.follow()
268 response = response.follow()
286 extensions_input = (
269 extensions_input = (
287 '<input id="extensions_evolve" '
270 '<input id="extensions_evolve" '
288 'name="extensions_evolve" type="checkbox" '
271 'name="extensions_evolve" type="checkbox" '
289 'value="True" checked="checked" />')
272 'value="True" checked="checked" />')
290 response.mustcontain(extensions_input)
273 response.mustcontain(extensions_input)
291
274
292 def test_has_a_section_for_pull_request_settings(self):
275 def test_has_a_section_for_pull_request_settings(self):
293 response = self.app.get(route_path('admin_settings_vcs'))
276 response = self.app.get(route_path('admin_settings_vcs'))
294 response.mustcontain('Pull Request Settings')
277 response.mustcontain('Pull Request Settings')
295
278
296 def test_has_an_input_for_invalidation_of_inline_comments(self):
279 def test_has_an_input_for_invalidation_of_inline_comments(self):
297 response = self.app.get(route_path('admin_settings_vcs'))
280 response = self.app.get(route_path('admin_settings_vcs'))
298 assert_response = response.assert_response()
281 assert_response = response.assert_response()
299 assert_response.one_element_exists(
282 assert_response.one_element_exists(
300 '[name=rhodecode_use_outdated_comments]')
283 '[name=rhodecode_use_outdated_comments]')
301
284
302 @pytest.mark.parametrize('new_value', [True, False])
285 @pytest.mark.parametrize('new_value', [True, False])
303 def test_allows_to_change_invalidation_of_inline_comments(
286 def test_allows_to_change_invalidation_of_inline_comments(
304 self, form_defaults, csrf_token, new_value):
287 self, form_defaults, csrf_token, new_value):
305 setting_key = 'use_outdated_comments'
288 setting_key = 'use_outdated_comments'
306 setting = SettingsModel().create_or_update_setting(
289 setting = SettingsModel().create_or_update_setting(
307 setting_key, not new_value, 'bool')
290 setting_key, not new_value, 'bool')
308 Session().add(setting)
291 Session().add(setting)
309 Session().commit()
292 Session().commit()
310
293
311 form_defaults.update({
294 form_defaults.update({
312 'csrf_token': csrf_token,
295 'csrf_token': csrf_token,
313 'rhodecode_use_outdated_comments': str(new_value),
296 'rhodecode_use_outdated_comments': str(new_value),
314 })
297 })
315 response = self.app.post(
298 response = self.app.post(
316 route_path('admin_settings_vcs_update'),
299 route_path('admin_settings_vcs_update'),
317 params=form_defaults,
300 params=form_defaults,
318 status=302)
301 status=302)
319 response = response.follow()
302 response = response.follow()
320 setting = SettingsModel().get_setting_by_name(setting_key)
303 setting = SettingsModel().get_setting_by_name(setting_key)
321 assert setting.app_settings_value is new_value
304 assert setting.app_settings_value is new_value
322
305
323 @pytest.mark.parametrize('new_value', [True, False])
306 @pytest.mark.parametrize('new_value', [True, False])
324 def test_allows_to_change_hg_rebase_merge_strategy(
307 def test_allows_to_change_hg_rebase_merge_strategy(
325 self, form_defaults, csrf_token, new_value):
308 self, form_defaults, csrf_token, new_value):
326 setting_key = 'hg_use_rebase_for_merging'
309 setting_key = 'hg_use_rebase_for_merging'
327
310
328 form_defaults.update({
311 form_defaults.update({
329 'csrf_token': csrf_token,
312 'csrf_token': csrf_token,
330 'rhodecode_' + setting_key: str(new_value),
313 'rhodecode_' + setting_key: str(new_value),
331 })
314 })
332
315
333 with mock.patch.dict(
316 with mock.patch.dict(
334 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
317 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
335 self.app.post(
318 self.app.post(
336 route_path('admin_settings_vcs_update'),
319 route_path('admin_settings_vcs_update'),
337 params=form_defaults,
320 params=form_defaults,
338 status=302)
321 status=302)
339
322
340 setting = SettingsModel().get_setting_by_name(setting_key)
323 setting = SettingsModel().get_setting_by_name(setting_key)
341 assert setting.app_settings_value is new_value
324 assert setting.app_settings_value is new_value
342
325
343 @pytest.fixture()
326 @pytest.fixture()
344 def disable_sql_cache(self, request):
327 def disable_sql_cache(self, request):
345 # patch _do_orm_execute so it returns None similar like if we don't use a cached query
328 # patch _do_orm_execute so it returns None similar like if we don't use a cached query
346 patcher = mock.patch(
329 patcher = mock.patch(
347 'rhodecode.lib.caching_query.ORMCache._do_orm_execute', return_value=None)
330 'rhodecode.lib.caching_query.ORMCache._do_orm_execute', return_value=None)
348 request.addfinalizer(patcher.stop)
331 request.addfinalizer(patcher.stop)
349 patcher.start()
332 patcher.start()
350
333
351 @pytest.fixture()
334 @pytest.fixture()
352 def form_defaults(self):
335 def form_defaults(self):
353 from rhodecode.apps.admin.views.settings import AdminSettingsView
336 from rhodecode.apps.admin.views.settings import AdminSettingsView
354 return AdminSettingsView._form_defaults()
337 return AdminSettingsView._form_defaults()
355
338
356 # TODO: johbo: What we really want is to checkpoint before a test run and
339 # TODO: johbo: What we really want is to checkpoint before a test run and
357 # reset the session afterwards.
340 # reset the session afterwards.
358 @pytest.fixture(scope='class', autouse=True)
341 @pytest.fixture(scope='class', autouse=True)
359 def cleanup_settings(self, request, baseapp):
342 def cleanup_settings(self, request, baseapp):
360 ui_id = RhodeCodeUi.ui_id
343 ui_id = RhodeCodeUi.ui_id
361 original_ids = [r.ui_id for r in RhodeCodeUi.query().with_entities(ui_id)]
344 original_ids = [r.ui_id for r in RhodeCodeUi.query().with_entities(ui_id)]
362
345
363 @request.addfinalizer
346 @request.addfinalizer
364 def cleanup():
347 def cleanup():
365 RhodeCodeUi.query().filter(
348 RhodeCodeUi.query().filter(
366 ui_id.notin_(original_ids)).delete(False)
349 ui_id.notin_(original_ids)).delete(False)
367
350
368
351
369 @pytest.mark.usefixtures('autologin_user', 'app')
352 @pytest.mark.usefixtures('autologin_user', 'app')
370 class TestLabsSettings(object):
353 class TestLabsSettings(object):
371 def test_get_settings_page_disabled(self):
354 def test_get_settings_page_disabled(self):
372 with mock.patch.dict(
355 with mock.patch.dict(
373 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
356 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
374
357
375 response = self.app.get(
358 response = self.app.get(
376 route_path('admin_settings_labs'), status=302)
359 route_path('admin_settings_labs'), status=302)
377
360
378 assert response.location.endswith(route_path('admin_settings'))
361 assert response.location.endswith(route_path('admin_settings'))
379
362
380 def test_get_settings_page_enabled(self):
363 def test_get_settings_page_enabled(self):
381 from rhodecode.apps.admin.views import settings
364 from rhodecode.apps.admin.views import settings
382 lab_settings = [
365 lab_settings = [
383 settings.LabSetting(
366 settings.LabSetting(
384 key='rhodecode_bool',
367 key='rhodecode_bool',
385 type='bool',
368 type='bool',
386 group='bool group',
369 group='bool group',
387 label='bool label',
370 label='bool label',
388 help='bool help'
371 help='bool help'
389 ),
372 ),
390 settings.LabSetting(
373 settings.LabSetting(
391 key='rhodecode_text',
374 key='rhodecode_text',
392 type='unicode',
375 type='unicode',
393 group='text group',
376 group='text group',
394 label='text label',
377 label='text label',
395 help='text help'
378 help='text help'
396 ),
379 ),
397 ]
380 ]
398 with mock.patch.dict(rhodecode.CONFIG,
381 with mock.patch.dict(rhodecode.CONFIG,
399 {'labs_settings_active': 'true'}):
382 {'labs_settings_active': 'true'}):
400 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
383 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
401 response = self.app.get(route_path('admin_settings_labs'))
384 response = self.app.get(route_path('admin_settings_labs'))
402
385
403 assert '<label>bool group:</label>' in response
386 assert '<label>bool group:</label>' in response
404 assert '<label for="rhodecode_bool">bool label</label>' in response
387 assert '<label for="rhodecode_bool">bool label</label>' in response
405 assert '<p class="help-block">bool help</p>' in response
388 assert '<p class="help-block">bool help</p>' in response
406 assert 'name="rhodecode_bool" type="checkbox"' in response
389 assert 'name="rhodecode_bool" type="checkbox"' in response
407
390
408 assert '<label>text group:</label>' in response
391 assert '<label>text group:</label>' in response
409 assert '<label for="rhodecode_text">text label</label>' in response
392 assert '<label for="rhodecode_text">text label</label>' in response
410 assert '<p class="help-block">text help</p>' in response
393 assert '<p class="help-block">text help</p>' in response
411 assert 'name="rhodecode_text" size="60" type="text"' in response
394 assert 'name="rhodecode_text" size="60" type="text"' in response
412
395
413
396
414 @pytest.mark.usefixtures('app')
397 @pytest.mark.usefixtures('app')
415 class TestOpenSourceLicenses(object):
398 class TestOpenSourceLicenses(object):
416
399
417 def test_records_are_displayed(self, autologin_user):
400 def test_records_are_displayed(self, autologin_user):
418 sample_licenses = [
401 sample_licenses = [
419 {
402 {
420 "license": [
403 "license": [
421 {
404 {
422 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
405 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
423 "shortName": "bsdOriginal",
406 "shortName": "bsdOriginal",
424 "spdxId": "BSD-4-Clause",
407 "spdxId": "BSD-4-Clause",
425 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
408 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
426 }
409 }
427 ],
410 ],
428 "name": "python2.7-coverage-3.7.1"
411 "name": "python2.7-coverage-3.7.1"
429 },
412 },
430 {
413 {
431 "license": [
414 "license": [
432 {
415 {
433 "fullName": "MIT License",
416 "fullName": "MIT License",
434 "shortName": "mit",
417 "shortName": "mit",
435 "spdxId": "MIT",
418 "spdxId": "MIT",
436 "url": "http://spdx.org/licenses/MIT.html"
419 "url": "http://spdx.org/licenses/MIT.html"
437 }
420 }
438 ],
421 ],
439 "name": "python2.7-bootstrapped-pip-9.0.1"
422 "name": "python2.7-bootstrapped-pip-9.0.1"
440 },
423 },
441 ]
424 ]
442 read_licenses_patch = mock.patch(
425 read_licenses_patch = mock.patch(
443 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
426 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
444 return_value=sample_licenses)
427 return_value=sample_licenses)
445 with read_licenses_patch:
428 with read_licenses_patch:
446 response = self.app.get(
429 response = self.app.get(
447 route_path('admin_settings_open_source'), status=200)
430 route_path('admin_settings_open_source'), status=200)
448
431
449 assert_response = response.assert_response()
432 assert_response = response.assert_response()
450 assert_response.element_contains(
433 assert_response.element_contains(
451 '.panel-heading', 'Licenses of Third Party Packages')
434 '.panel-heading', 'Licenses of Third Party Packages')
452 for license_data in sample_licenses:
435 for license_data in sample_licenses:
453 response.mustcontain(license_data["license"][0]["spdxId"])
436 response.mustcontain(license_data["license"][0]["spdxId"])
454 assert_response.element_contains('.panel-body', license_data["name"])
437 assert_response.element_contains('.panel-body', license_data["name"])
455
438
456 def test_records_can_be_read(self, autologin_user):
439 def test_records_can_be_read(self, autologin_user):
457 response = self.app.get(
440 response = self.app.get(
458 route_path('admin_settings_open_source'), status=200)
441 route_path('admin_settings_open_source'), status=200)
459 assert_response = response.assert_response()
442 assert_response = response.assert_response()
460 assert_response.element_contains(
443 assert_response.element_contains(
461 '.panel-heading', 'Licenses of Third Party Packages')
444 '.panel-heading', 'Licenses of Third Party Packages')
462
445
463 def test_forbidden_when_normal_user(self, autologin_regular_user):
446 def test_forbidden_when_normal_user(self, autologin_regular_user):
464 self.app.get(
447 self.app.get(
465 route_path('admin_settings_open_source'), status=404)
448 route_path('admin_settings_open_source'), status=404)
466
449
467
450
468 @pytest.mark.usefixtures('app')
451 @pytest.mark.usefixtures('app')
469 class TestUserSessions(object):
452 class TestUserSessions(object):
470
453
471 def test_forbidden_when_normal_user(self, autologin_regular_user):
454 def test_forbidden_when_normal_user(self, autologin_regular_user):
472 self.app.get(route_path('admin_settings_sessions'), status=404)
455 self.app.get(route_path('admin_settings_sessions'), status=404)
473
456
474 def test_show_sessions_page(self, autologin_user):
457 def test_show_sessions_page(self, autologin_user):
475 response = self.app.get(route_path('admin_settings_sessions'), status=200)
458 response = self.app.get(route_path('admin_settings_sessions'), status=200)
476 response.mustcontain('file')
459 response.mustcontain('file')
477
460
478 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
461 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
479
462
480 post_data = {
463 post_data = {
481 'csrf_token': csrf_token,
464 'csrf_token': csrf_token,
482 'expire_days': '60'
465 'expire_days': '60'
483 }
466 }
484 response = self.app.post(
467 response = self.app.post(
485 route_path('admin_settings_sessions_cleanup'), params=post_data,
468 route_path('admin_settings_sessions_cleanup'), params=post_data,
486 status=302)
469 status=302)
487 assert_session_flash(response, 'Cleaned up old sessions')
470 assert_session_flash(response, 'Cleaned up old sessions')
488
471
489
472
490 @pytest.mark.usefixtures('app')
473 @pytest.mark.usefixtures('app')
491 class TestAdminSystemInfo(object):
474 class TestAdminSystemInfo(object):
492
475
493 def test_forbidden_when_normal_user(self, autologin_regular_user):
476 def test_forbidden_when_normal_user(self, autologin_regular_user):
494 self.app.get(route_path('admin_settings_system'), status=404)
477 self.app.get(route_path('admin_settings_system'), status=404)
495
478
496 def test_system_info_page(self, autologin_user):
479 def test_system_info_page(self, autologin_user):
497 response = self.app.get(route_path('admin_settings_system'))
480 response = self.app.get(route_path('admin_settings_system'))
498 response.mustcontain('RhodeCode Community Edition, version {}'.format(
481 response.mustcontain('RhodeCode Community Edition, version {}'.format(
499 rhodecode.__version__))
482 rhodecode.__version__))
500
483
501 def test_system_update_new_version(self, autologin_user):
484 def test_system_update_new_version(self, autologin_user):
502 update_data = {
485 update_data = {
503 'versions': [
486 'versions': [
504 {
487 {
505 'version': '100.3.1415926535',
488 'version': '100.3.1415926535',
506 'general': 'The latest version we are ever going to ship'
489 'general': 'The latest version we are ever going to ship'
507 },
490 },
508 {
491 {
509 'version': '0.0.0',
492 'version': '0.0.0',
510 'general': 'The first version we ever shipped'
493 'general': 'The first version we ever shipped'
511 }
494 }
512 ]
495 ]
513 }
496 }
514 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
497 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
515 response = self.app.get(route_path('admin_settings_system_update'))
498 response = self.app.get(route_path('admin_settings_system_update'))
516 response.mustcontain('A <b>new version</b> is available')
499 response.mustcontain('A <b>new version</b> is available')
517
500
518 def test_system_update_nothing_new(self, autologin_user):
501 def test_system_update_nothing_new(self, autologin_user):
519 update_data = {
502 update_data = {
520 'versions': [
503 'versions': [
521 {
504 {
522 'version': '0.0.0',
505 'version': '0.0.0',
523 'general': 'The first version we ever shipped'
506 'general': 'The first version we ever shipped'
524 }
507 }
525 ]
508 ]
526 }
509 }
527 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
510 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
528 response = self.app.get(route_path('admin_settings_system_update'))
511 response = self.app.get(route_path('admin_settings_system_update'))
529 response.mustcontain(
512 response.mustcontain(
530 'This instance is already running the <b>latest</b> stable version')
513 'This instance is already running the <b>latest</b> stable version')
531
514
532 def test_system_update_bad_response(self, autologin_user):
515 def test_system_update_bad_response(self, autologin_user):
533 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
516 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
534 response = self.app.get(route_path('admin_settings_system_update'))
517 response = self.app.get(route_path('admin_settings_system_update'))
535 response.mustcontain(
518 response.mustcontain(
536 'Bad data sent from update server')
519 'Bad data sent from update server')
537
520
538
521
539 @pytest.mark.usefixtures("app")
522 @pytest.mark.usefixtures("app")
540 class TestAdminSettingsIssueTracker(object):
523 class TestAdminSettingsIssueTracker(object):
541 RC_PREFIX = 'rhodecode_'
524 RC_PREFIX = 'rhodecode_'
542 SHORT_PATTERN_KEY = 'issuetracker_pat_'
525 SHORT_PATTERN_KEY = 'issuetracker_pat_'
543 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
526 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
544 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
527 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
545
528
546 def test_issuetracker_index(self, autologin_user):
529 def test_issuetracker_index(self, autologin_user):
547 response = self.app.get(route_path('admin_settings_issuetracker'))
530 response = self.app.get(route_path('admin_settings_issuetracker'))
548 assert response.status_code == 200
531 assert response.status_code == 200
549
532
550 def test_add_empty_issuetracker_pattern(
533 def test_add_empty_issuetracker_pattern(
551 self, request, autologin_user, csrf_token):
534 self, request, autologin_user, csrf_token):
552 post_url = route_path('admin_settings_issuetracker_update')
535 post_url = route_path('admin_settings_issuetracker_update')
553 post_data = {
536 post_data = {
554 'csrf_token': csrf_token
537 'csrf_token': csrf_token
555 }
538 }
556 self.app.post(post_url, post_data, status=302)
539 self.app.post(post_url, post_data, status=302)
557
540
558 def test_add_issuetracker_pattern(
541 def test_add_issuetracker_pattern(
559 self, request, autologin_user, csrf_token):
542 self, request, autologin_user, csrf_token):
560 pattern = 'issuetracker_pat'
543 pattern = 'issuetracker_pat'
561 another_pattern = pattern+'1'
544 another_pattern = pattern+'1'
562 post_url = route_path('admin_settings_issuetracker_update')
545 post_url = route_path('admin_settings_issuetracker_update')
563 post_data = {
546 post_data = {
564 'new_pattern_pattern_0': pattern,
547 'new_pattern_pattern_0': pattern,
565 'new_pattern_url_0': 'http://url',
548 'new_pattern_url_0': 'http://url',
566 'new_pattern_prefix_0': 'prefix',
549 'new_pattern_prefix_0': 'prefix',
567 'new_pattern_description_0': 'description',
550 'new_pattern_description_0': 'description',
568 'new_pattern_pattern_1': another_pattern,
551 'new_pattern_pattern_1': another_pattern,
569 'new_pattern_url_1': 'https://url1',
552 'new_pattern_url_1': 'https://url1',
570 'new_pattern_prefix_1': 'prefix1',
553 'new_pattern_prefix_1': 'prefix1',
571 'new_pattern_description_1': 'description1',
554 'new_pattern_description_1': 'description1',
572 'csrf_token': csrf_token
555 'csrf_token': csrf_token
573 }
556 }
574 self.app.post(post_url, post_data, status=302)
557 self.app.post(post_url, post_data, status=302)
575 settings = SettingsModel().get_all_settings()
558 settings = SettingsModel().get_all_settings()
576 self.uid = md5_safe(pattern)
559 self.uid = md5_safe(pattern)
577 assert settings[self.PATTERN_KEY+self.uid] == pattern
560 assert settings[self.PATTERN_KEY+self.uid] == pattern
578 self.another_uid = md5_safe(another_pattern)
561 self.another_uid = md5_safe(another_pattern)
579 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
562 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
580
563
581 @request.addfinalizer
564 @request.addfinalizer
582 def cleanup():
565 def cleanup():
583 defaults = SettingsModel().get_all_settings()
566 defaults = SettingsModel().get_all_settings()
584
567
585 entries = [name for name in defaults if (
568 entries = [name for name in defaults if (
586 (self.uid in name) or (self.another_uid in name))]
569 (self.uid in name) or (self.another_uid in name))]
587 start = len(self.RC_PREFIX)
570 start = len(self.RC_PREFIX)
588 for del_key in entries:
571 for del_key in entries:
589 # TODO: anderson: get_by_name needs name without prefix
572 # TODO: anderson: get_by_name needs name without prefix
590 entry = SettingsModel().get_setting_by_name(del_key[start:])
573 entry = SettingsModel().get_setting_by_name(del_key[start:])
591 Session().delete(entry)
574 Session().delete(entry)
592
575
593 Session().commit()
576 Session().commit()
594
577
595 def test_edit_issuetracker_pattern(
578 def test_edit_issuetracker_pattern(
596 self, autologin_user, backend, csrf_token, request):
579 self, autologin_user, backend, csrf_token, request):
597
580
598 old_pattern = 'issuetracker_pat1'
581 old_pattern = 'issuetracker_pat1'
599 old_uid = md5_safe(old_pattern)
582 old_uid = md5_safe(old_pattern)
600
583
601 post_url = route_path('admin_settings_issuetracker_update')
584 post_url = route_path('admin_settings_issuetracker_update')
602 post_data = {
585 post_data = {
603 'new_pattern_pattern_0': old_pattern,
586 'new_pattern_pattern_0': old_pattern,
604 'new_pattern_url_0': 'http://url',
587 'new_pattern_url_0': 'http://url',
605 'new_pattern_prefix_0': 'prefix',
588 'new_pattern_prefix_0': 'prefix',
606 'new_pattern_description_0': 'description',
589 'new_pattern_description_0': 'description',
607
590
608 'csrf_token': csrf_token
591 'csrf_token': csrf_token
609 }
592 }
610 self.app.post(post_url, post_data, status=302)
593 self.app.post(post_url, post_data, status=302)
611
594
612 new_pattern = 'issuetracker_pat1_edited'
595 new_pattern = 'issuetracker_pat1_edited'
613 self.new_uid = md5_safe(new_pattern)
596 self.new_uid = md5_safe(new_pattern)
614
597
615 post_url = route_path('admin_settings_issuetracker_update')
598 post_url = route_path('admin_settings_issuetracker_update')
616 post_data = {
599 post_data = {
617 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
600 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
618 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
601 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
619 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
602 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
620 'new_pattern_description_{}'.format(old_uid): 'description_edited',
603 'new_pattern_description_{}'.format(old_uid): 'description_edited',
621 'uid': old_uid,
604 'uid': old_uid,
622 'csrf_token': csrf_token
605 'csrf_token': csrf_token
623 }
606 }
624 self.app.post(post_url, post_data, status=302)
607 self.app.post(post_url, post_data, status=302)
625
608
626 settings = SettingsModel().get_all_settings()
609 settings = SettingsModel().get_all_settings()
627 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
610 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
628 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
611 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
629 assert self.PATTERN_KEY+old_uid not in settings
612 assert self.PATTERN_KEY+old_uid not in settings
630
613
631 @request.addfinalizer
614 @request.addfinalizer
632 def cleanup():
615 def cleanup():
633 IssueTrackerSettingsModel().delete_entries(old_uid)
616 IssueTrackerSettingsModel().delete_entries(old_uid)
634 IssueTrackerSettingsModel().delete_entries(self.new_uid)
617 IssueTrackerSettingsModel().delete_entries(self.new_uid)
635
618
636 def test_replace_issuetracker_pattern_description(
619 def test_replace_issuetracker_pattern_description(
637 self, autologin_user, csrf_token, request, settings_util):
620 self, autologin_user, csrf_token, request, settings_util):
638 prefix = 'issuetracker'
621 prefix = 'issuetracker'
639 pattern = 'issuetracker_pat'
622 pattern = 'issuetracker_pat'
640 self.uid = md5_safe(pattern)
623 self.uid = md5_safe(pattern)
641 pattern_key = '_'.join([prefix, 'pat', self.uid])
624 pattern_key = '_'.join([prefix, 'pat', self.uid])
642 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
625 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
643 desc_key = '_'.join([prefix, 'desc', self.uid])
626 desc_key = '_'.join([prefix, 'desc', self.uid])
644 rc_desc_key = '_'.join(['rhodecode', desc_key])
627 rc_desc_key = '_'.join(['rhodecode', desc_key])
645 new_description = 'new_description'
628 new_description = 'new_description'
646
629
647 settings_util.create_rhodecode_setting(
630 settings_util.create_rhodecode_setting(
648 pattern_key, pattern, 'unicode', cleanup=False)
631 pattern_key, pattern, 'unicode', cleanup=False)
649 settings_util.create_rhodecode_setting(
632 settings_util.create_rhodecode_setting(
650 desc_key, 'old description', 'unicode', cleanup=False)
633 desc_key, 'old description', 'unicode', cleanup=False)
651
634
652 post_url = route_path('admin_settings_issuetracker_update')
635 post_url = route_path('admin_settings_issuetracker_update')
653 post_data = {
636 post_data = {
654 'new_pattern_pattern_0': pattern,
637 'new_pattern_pattern_0': pattern,
655 'new_pattern_url_0': 'https://url',
638 'new_pattern_url_0': 'https://url',
656 'new_pattern_prefix_0': 'prefix',
639 'new_pattern_prefix_0': 'prefix',
657 'new_pattern_description_0': new_description,
640 'new_pattern_description_0': new_description,
658 'uid': self.uid,
641 'uid': self.uid,
659 'csrf_token': csrf_token
642 'csrf_token': csrf_token
660 }
643 }
661 self.app.post(post_url, post_data, status=302)
644 self.app.post(post_url, post_data, status=302)
662 settings = SettingsModel().get_all_settings()
645 settings = SettingsModel().get_all_settings()
663 assert settings[rc_pattern_key] == pattern
646 assert settings[rc_pattern_key] == pattern
664 assert settings[rc_desc_key] == new_description
647 assert settings[rc_desc_key] == new_description
665
648
666 @request.addfinalizer
649 @request.addfinalizer
667 def cleanup():
650 def cleanup():
668 IssueTrackerSettingsModel().delete_entries(self.uid)
651 IssueTrackerSettingsModel().delete_entries(self.uid)
669
652
670 def test_delete_issuetracker_pattern(
653 def test_delete_issuetracker_pattern(
671 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
654 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
672
655
673 old_pattern = 'issuetracker_pat_deleted'
656 old_pattern = 'issuetracker_pat_deleted'
674 old_uid = md5_safe(old_pattern)
657 old_uid = md5_safe(old_pattern)
675
658
676 post_url = route_path('admin_settings_issuetracker_update')
659 post_url = route_path('admin_settings_issuetracker_update')
677 post_data = {
660 post_data = {
678 'new_pattern_pattern_0': old_pattern,
661 'new_pattern_pattern_0': old_pattern,
679 'new_pattern_url_0': 'http://url',
662 'new_pattern_url_0': 'http://url',
680 'new_pattern_prefix_0': 'prefix',
663 'new_pattern_prefix_0': 'prefix',
681 'new_pattern_description_0': 'description',
664 'new_pattern_description_0': 'description',
682
665
683 'csrf_token': csrf_token
666 'csrf_token': csrf_token
684 }
667 }
685 self.app.post(post_url, post_data, status=302)
668 self.app.post(post_url, post_data, status=302)
686
669
687 post_url = route_path('admin_settings_issuetracker_delete')
670 post_url = route_path('admin_settings_issuetracker_delete')
688 post_data = {
671 post_data = {
689 'uid': old_uid,
672 'uid': old_uid,
690 'csrf_token': csrf_token
673 'csrf_token': csrf_token
691 }
674 }
692 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
675 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
693 settings = SettingsModel().get_all_settings()
676 settings = SettingsModel().get_all_settings()
694 assert self.PATTERN_KEY+old_uid not in settings
677 assert self.PATTERN_KEY+old_uid not in settings
695 assert self.DESC_KEY + old_uid not in settings
678 assert self.DESC_KEY + old_uid not in settings
@@ -1,687 +1,679 b''
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 Database creation, and setup module for RhodeCode Enterprise. Used for creation
20 Database creation, and setup module for RhodeCode Enterprise. Used for creation
21 of database as well as for migration operations
21 of database as well as for migration operations
22 """
22 """
23
23
24 import os
24 import os
25 import sys
25 import sys
26 import time
26 import time
27 import uuid
27 import uuid
28 import logging
28 import logging
29 import getpass
29 import getpass
30 from os.path import dirname as dn, join as jn
30 from os.path import dirname as dn, join as jn
31
31
32 from sqlalchemy.engine import create_engine
32 from sqlalchemy.engine import create_engine
33
33
34 from rhodecode import __dbversion__
34 from rhodecode import __dbversion__
35 from rhodecode.model import init_model
35 from rhodecode.model import init_model
36 from rhodecode.model.user import UserModel
36 from rhodecode.model.user import UserModel
37 from rhodecode.model.db import (
37 from rhodecode.model.db import (
38 User, Permission, RhodeCodeUi, RhodeCodeSetting, UserToPerm,
38 User, Permission, RhodeCodeUi, RhodeCodeSetting, UserToPerm,
39 DbMigrateVersion, RepoGroup, UserRepoGroupToPerm, CacheKey, Repository)
39 DbMigrateVersion, RepoGroup, UserRepoGroupToPerm, CacheKey, Repository)
40 from rhodecode.model.meta import Session, Base
40 from rhodecode.model.meta import Session, Base
41 from rhodecode.model.permission import PermissionModel
41 from rhodecode.model.permission import PermissionModel
42 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.repo import RepoModel
43 from rhodecode.model.repo_group import RepoGroupModel
43 from rhodecode.model.repo_group import RepoGroupModel
44 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 def notify(msg):
50 def notify(msg):
51 """
51 """
52 Notification for migrations messages
52 Notification for migrations messages
53 """
53 """
54 ml = len(msg) + (4 * 2)
54 ml = len(msg) + (4 * 2)
55 print((('\n%s\n*** %s ***\n%s' % ('*' * ml, msg, '*' * ml)).upper()))
55 print((('\n%s\n*** %s ***\n%s' % ('*' * ml, msg, '*' * ml)).upper()))
56
56
57
57
58 class DbManage(object):
58 class DbManage(object):
59
59
60 def __init__(self, log_sql, dbconf, root, tests=False,
60 def __init__(self, log_sql, dbconf, root, tests=False,
61 SESSION=None, cli_args=None, enc_key=b''):
61 SESSION=None, cli_args=None, enc_key=b''):
62
62
63 self.dbname = dbconf.split('/')[-1]
63 self.dbname = dbconf.split('/')[-1]
64 self.tests = tests
64 self.tests = tests
65 self.root = root
65 self.root = root
66 self.dburi = dbconf
66 self.dburi = dbconf
67 self.log_sql = log_sql
67 self.log_sql = log_sql
68 self.cli_args = cli_args or {}
68 self.cli_args = cli_args or {}
69 self.sa = None
69 self.sa = None
70 self.engine = None
70 self.engine = None
71 self.enc_key = enc_key
71 self.enc_key = enc_key
72 # sets .sa .engine
72 # sets .sa .engine
73 self.init_db(SESSION=SESSION)
73 self.init_db(SESSION=SESSION)
74
74
75 self.ask_ok = self.get_ask_ok_func(self.cli_args.get('force_ask'))
75 self.ask_ok = self.get_ask_ok_func(self.cli_args.get('force_ask'))
76
76
77 def db_exists(self):
77 def db_exists(self):
78 if not self.sa:
78 if not self.sa:
79 self.init_db()
79 self.init_db()
80 try:
80 try:
81 self.sa.query(RhodeCodeUi)\
81 self.sa.query(RhodeCodeUi)\
82 .filter(RhodeCodeUi.ui_key == '/')\
82 .filter(RhodeCodeUi.ui_key == '/')\
83 .scalar()
83 .scalar()
84 return True
84 return True
85 except Exception:
85 except Exception:
86 return False
86 return False
87 finally:
87 finally:
88 self.sa.rollback()
88 self.sa.rollback()
89
89
90 def get_ask_ok_func(self, param):
90 def get_ask_ok_func(self, param):
91 if param not in [None]:
91 if param not in [None]:
92 # return a function lambda that has a default set to param
92 # return a function lambda that has a default set to param
93 return lambda *args, **kwargs: param
93 return lambda *args, **kwargs: param
94 else:
94 else:
95 from rhodecode.lib.utils import ask_ok
95 from rhodecode.lib.utils import ask_ok
96 return ask_ok
96 return ask_ok
97
97
98 def init_db(self, SESSION=None):
98 def init_db(self, SESSION=None):
99
99
100 if SESSION:
100 if SESSION:
101 self.sa = SESSION
101 self.sa = SESSION
102 self.engine = SESSION.bind
102 self.engine = SESSION.bind
103 else:
103 else:
104 # init new sessions
104 # init new sessions
105 engine = create_engine(self.dburi, echo=self.log_sql)
105 engine = create_engine(self.dburi, echo=self.log_sql)
106 init_model(engine, encryption_key=self.enc_key)
106 init_model(engine, encryption_key=self.enc_key)
107 self.sa = Session()
107 self.sa = Session()
108 self.engine = engine
108 self.engine = engine
109
109
110 def create_tables(self, override=False):
110 def create_tables(self, override=False):
111 """
111 """
112 Create a auth database
112 Create a auth database
113 """
113 """
114
114
115 log.info("Existing database with the same name is going to be destroyed.")
115 log.info("Existing database with the same name is going to be destroyed.")
116 log.info("Setup command will run DROP ALL command on that database.")
116 log.info("Setup command will run DROP ALL command on that database.")
117 engine = self.engine
117 engine = self.engine
118
118
119 if self.tests:
119 if self.tests:
120 destroy = True
120 destroy = True
121 else:
121 else:
122 destroy = self.ask_ok('Are you sure that you want to destroy the old database? [y/n]')
122 destroy = self.ask_ok('Are you sure that you want to destroy the old database? [y/n]')
123 if not destroy:
123 if not destroy:
124 log.info('db tables bootstrap: Nothing done.')
124 log.info('db tables bootstrap: Nothing done.')
125 sys.exit(0)
125 sys.exit(0)
126 if destroy:
126 if destroy:
127 Base.metadata.drop_all(bind=engine)
127 Base.metadata.drop_all(bind=engine)
128
128
129 checkfirst = not override
129 checkfirst = not override
130 Base.metadata.create_all(bind=engine, checkfirst=checkfirst)
130 Base.metadata.create_all(bind=engine, checkfirst=checkfirst)
131 log.info('Created tables for %s', self.dbname)
131 log.info('Created tables for %s', self.dbname)
132
132
133 def set_db_version(self):
133 def set_db_version(self):
134 ver = DbMigrateVersion()
134 ver = DbMigrateVersion()
135 ver.version = __dbversion__
135 ver.version = __dbversion__
136 ver.repository_id = 'rhodecode_db_migrations'
136 ver.repository_id = 'rhodecode_db_migrations'
137 ver.repository_path = 'versions'
137 ver.repository_path = 'versions'
138 self.sa.add(ver)
138 self.sa.add(ver)
139 log.info('db version set to: %s', __dbversion__)
139 log.info('db version set to: %s', __dbversion__)
140
140
141 def run_post_migration_tasks(self):
141 def run_post_migration_tasks(self):
142 """
142 """
143 Run various tasks before actually doing migrations
143 Run various tasks before actually doing migrations
144 """
144 """
145 # delete cache keys on each upgrade
145 # delete cache keys on each upgrade
146 total = CacheKey.query().count()
146 total = CacheKey.query().count()
147 log.info("Deleting (%s) cache keys now...", total)
147 log.info("Deleting (%s) cache keys now...", total)
148 CacheKey.delete_all_cache()
148 CacheKey.delete_all_cache()
149
149
150 def upgrade(self, version=None):
150 def upgrade(self, version=None):
151 """
151 """
152 Upgrades given database schema to given revision following
152 Upgrades given database schema to given revision following
153 all needed steps, to perform the upgrade
153 all needed steps, to perform the upgrade
154
154
155 """
155 """
156
156
157 from rhodecode.lib.dbmigrate.migrate.versioning import api
157 from rhodecode.lib.dbmigrate.migrate.versioning import api
158 from rhodecode.lib.dbmigrate.migrate.exceptions import DatabaseNotControlledError
158 from rhodecode.lib.dbmigrate.migrate.exceptions import DatabaseNotControlledError
159
159
160 if 'sqlite' in self.dburi:
160 if 'sqlite' in self.dburi:
161 print(
161 print(
162 '********************** WARNING **********************\n'
162 '********************** WARNING **********************\n'
163 'Make sure your version of sqlite is at least 3.7.X. \n'
163 'Make sure your version of sqlite is at least 3.7.X. \n'
164 'Earlier versions are known to fail on some migrations\n'
164 'Earlier versions are known to fail on some migrations\n'
165 '*****************************************************\n')
165 '*****************************************************\n')
166
166
167 upgrade = self.ask_ok(
167 upgrade = self.ask_ok(
168 'You are about to perform a database upgrade. Make '
168 'You are about to perform a database upgrade. Make '
169 'sure you have backed up your database. '
169 'sure you have backed up your database. '
170 'Continue ? [y/n]')
170 'Continue ? [y/n]')
171 if not upgrade:
171 if not upgrade:
172 log.info('No upgrade performed')
172 log.info('No upgrade performed')
173 sys.exit(0)
173 sys.exit(0)
174
174
175 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
175 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
176 'rhodecode/lib/dbmigrate')
176 'rhodecode/lib/dbmigrate')
177 db_uri = self.dburi
177 db_uri = self.dburi
178
178
179 if version:
179 if version:
180 DbMigrateVersion.set_version(version)
180 DbMigrateVersion.set_version(version)
181
181
182 try:
182 try:
183 curr_version = api.db_version(db_uri, repository_path)
183 curr_version = api.db_version(db_uri, repository_path)
184 msg = (f'Found current database db_uri under version '
184 msg = (f'Found current database db_uri under version '
185 f'control with version {curr_version}')
185 f'control with version {curr_version}')
186
186
187 except (RuntimeError, DatabaseNotControlledError):
187 except (RuntimeError, DatabaseNotControlledError):
188 curr_version = 1
188 curr_version = 1
189 msg = f'Current database is not under version control. ' \
189 msg = f'Current database is not under version control. ' \
190 f'Setting as version {curr_version}'
190 f'Setting as version {curr_version}'
191 api.version_control(db_uri, repository_path, curr_version)
191 api.version_control(db_uri, repository_path, curr_version)
192
192
193 notify(msg)
193 notify(msg)
194
194
195 if curr_version == __dbversion__:
195 if curr_version == __dbversion__:
196 log.info('This database is already at the newest version')
196 log.info('This database is already at the newest version')
197 sys.exit(0)
197 sys.exit(0)
198
198
199 upgrade_steps = list(range(curr_version + 1, __dbversion__ + 1))
199 upgrade_steps = list(range(curr_version + 1, __dbversion__ + 1))
200 notify(f'attempting to upgrade database from '
200 notify(f'attempting to upgrade database from '
201 f'version {curr_version} to version {__dbversion__}')
201 f'version {curr_version} to version {__dbversion__}')
202
202
203 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
203 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
204 _step = None
204 _step = None
205 for step in upgrade_steps:
205 for step in upgrade_steps:
206 notify(f'performing upgrade step {step}')
206 notify(f'performing upgrade step {step}')
207 time.sleep(0.5)
207 time.sleep(0.5)
208
208
209 api.upgrade(db_uri, repository_path, step)
209 api.upgrade(db_uri, repository_path, step)
210 self.sa.rollback()
210 self.sa.rollback()
211 notify(f'schema upgrade for step {step} completed')
211 notify(f'schema upgrade for step {step} completed')
212
212
213 _step = step
213 _step = step
214
214
215 self.run_post_migration_tasks()
215 self.run_post_migration_tasks()
216 notify(f'upgrade to version {step} successful')
216 notify(f'upgrade to version {step} successful')
217
217
218 def fix_repo_paths(self):
218 def fix_repo_paths(self):
219 """
219 """
220 Fixes an old RhodeCode version path into new one without a '*'
220 Fixes an old RhodeCode version path into new one without a '*'
221 """
221 """
222
222
223 paths = self.sa.query(RhodeCodeUi)\
223 paths = self.sa.query(RhodeCodeUi)\
224 .filter(RhodeCodeUi.ui_key == '/')\
224 .filter(RhodeCodeUi.ui_key == '/')\
225 .scalar()
225 .scalar()
226
226
227 paths.ui_value = paths.ui_value.replace('*', '')
227 paths.ui_value = paths.ui_value.replace('*', '')
228
228
229 try:
229 try:
230 self.sa.add(paths)
230 self.sa.add(paths)
231 self.sa.commit()
231 self.sa.commit()
232 except Exception:
232 except Exception:
233 self.sa.rollback()
233 self.sa.rollback()
234 raise
234 raise
235
235
236 def fix_default_user(self):
236 def fix_default_user(self):
237 """
237 """
238 Fixes an old default user with some 'nicer' default values,
238 Fixes an old default user with some 'nicer' default values,
239 used mostly for anonymous access
239 used mostly for anonymous access
240 """
240 """
241 def_user = self.sa.query(User)\
241 def_user = self.sa.query(User)\
242 .filter(User.username == User.DEFAULT_USER)\
242 .filter(User.username == User.DEFAULT_USER)\
243 .one()
243 .one()
244
244
245 def_user.name = 'Anonymous'
245 def_user.name = 'Anonymous'
246 def_user.lastname = 'User'
246 def_user.lastname = 'User'
247 def_user.email = User.DEFAULT_USER_EMAIL
247 def_user.email = User.DEFAULT_USER_EMAIL
248
248
249 try:
249 try:
250 self.sa.add(def_user)
250 self.sa.add(def_user)
251 self.sa.commit()
251 self.sa.commit()
252 except Exception:
252 except Exception:
253 self.sa.rollback()
253 self.sa.rollback()
254 raise
254 raise
255
255
256 def fix_settings(self):
256 def fix_settings(self):
257 """
257 """
258 Fixes rhodecode settings and adds ga_code key for google analytics
258 Fixes rhodecode settings and adds ga_code key for google analytics
259 """
259 """
260
260
261 hgsettings3 = RhodeCodeSetting('ga_code', '')
261 hgsettings3 = RhodeCodeSetting('ga_code', '')
262
262
263 try:
263 try:
264 self.sa.add(hgsettings3)
264 self.sa.add(hgsettings3)
265 self.sa.commit()
265 self.sa.commit()
266 except Exception:
266 except Exception:
267 self.sa.rollback()
267 self.sa.rollback()
268 raise
268 raise
269
269
270 def create_admin_and_prompt(self):
270 def create_admin_and_prompt(self):
271
271
272 # defaults
272 # defaults
273 defaults = self.cli_args
273 defaults = self.cli_args
274 username = defaults.get('username')
274 username = defaults.get('username')
275 password = defaults.get('password')
275 password = defaults.get('password')
276 email = defaults.get('email')
276 email = defaults.get('email')
277
277
278 if username is None:
278 if username is None:
279 username = input('Specify admin username:')
279 username = input('Specify admin username:')
280 if password is None:
280 if password is None:
281 password = self._get_admin_password()
281 password = self._get_admin_password()
282 if not password:
282 if not password:
283 # second try
283 # second try
284 password = self._get_admin_password()
284 password = self._get_admin_password()
285 if not password:
285 if not password:
286 sys.exit()
286 sys.exit()
287 if email is None:
287 if email is None:
288 email = input('Specify admin email:')
288 email = input('Specify admin email:')
289 api_key = self.cli_args.get('api_key')
289 api_key = self.cli_args.get('api_key')
290 self.create_user(username, password, email, True,
290 self.create_user(username, password, email, True,
291 strict_creation_check=False,
291 strict_creation_check=False,
292 api_key=api_key)
292 api_key=api_key)
293
293
294 def _get_admin_password(self):
294 def _get_admin_password(self):
295 password = getpass.getpass('Specify admin password '
295 password = getpass.getpass('Specify admin password '
296 '(min 6 chars):')
296 '(min 6 chars):')
297 confirm = getpass.getpass('Confirm password:')
297 confirm = getpass.getpass('Confirm password:')
298
298
299 if password != confirm:
299 if password != confirm:
300 log.error('passwords mismatch')
300 log.error('passwords mismatch')
301 return False
301 return False
302 if len(password) < 6:
302 if len(password) < 6:
303 log.error('password is too short - use at least 6 characters')
303 log.error('password is too short - use at least 6 characters')
304 return False
304 return False
305
305
306 return password
306 return password
307
307
308 def create_test_admin_and_users(self):
308 def create_test_admin_and_users(self):
309 log.info('creating admin and regular test users')
309 log.info('creating admin and regular test users')
310 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
310 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
311 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
311 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
312 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
312 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
313 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
313 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
314 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
314 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
315
315
316 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
316 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
317 TEST_USER_ADMIN_EMAIL, True, api_key=True)
317 TEST_USER_ADMIN_EMAIL, True, api_key=True)
318
318
319 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
319 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
320 TEST_USER_REGULAR_EMAIL, False, api_key=True)
320 TEST_USER_REGULAR_EMAIL, False, api_key=True)
321
321
322 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
322 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
323 TEST_USER_REGULAR2_EMAIL, False, api_key=True)
323 TEST_USER_REGULAR2_EMAIL, False, api_key=True)
324
324
325 def create_ui_settings(self, repo_store_path):
325 def create_ui_settings(self, repo_store_path):
326 """
326 """
327 Creates ui settings, fills out hooks
327 Creates ui settings, fills out hooks
328 and disables dotencode
328 and disables dotencode
329 """
329 """
330 settings_model = SettingsModel(sa=self.sa)
330 settings_model = SettingsModel(sa=self.sa)
331 from rhodecode.lib.vcs.backends.hg import largefiles_store
331 from rhodecode.lib.vcs.backends.hg import largefiles_store
332 from rhodecode.lib.vcs.backends.git import lfs_store
332 from rhodecode.lib.vcs.backends.git import lfs_store
333
333
334 # Build HOOKS
334 # Build HOOKS
335 hooks = [
335 hooks = [
336 (RhodeCodeUi.HOOK_REPO_SIZE, 'python:vcsserver.hooks.repo_size'),
336 (RhodeCodeUi.HOOK_REPO_SIZE, 'python:vcsserver.hooks.repo_size'),
337
337
338 # HG
338 # HG
339 (RhodeCodeUi.HOOK_PRE_PULL, 'python:vcsserver.hooks.pre_pull'),
339 (RhodeCodeUi.HOOK_PRE_PULL, 'python:vcsserver.hooks.pre_pull'),
340 (RhodeCodeUi.HOOK_PULL, 'python:vcsserver.hooks.log_pull_action'),
340 (RhodeCodeUi.HOOK_PULL, 'python:vcsserver.hooks.log_pull_action'),
341 (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'),
341 (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'),
342 (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'),
342 (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'),
343 (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'),
343 (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'),
344 (RhodeCodeUi.HOOK_PUSH_KEY, 'python:vcsserver.hooks.key_push'),
344 (RhodeCodeUi.HOOK_PUSH_KEY, 'python:vcsserver.hooks.key_push'),
345
345
346 ]
346 ]
347
347
348 for key, value in hooks:
348 for key, value in hooks:
349 hook_obj = settings_model.get_ui_by_key(key)
349 hook_obj = settings_model.get_ui_by_key(key)
350 hooks2 = hook_obj if hook_obj else RhodeCodeUi()
350 hooks2 = hook_obj if hook_obj else RhodeCodeUi()
351 hooks2.ui_section = 'hooks'
351 hooks2.ui_section = 'hooks'
352 hooks2.ui_key = key
352 hooks2.ui_key = key
353 hooks2.ui_value = value
353 hooks2.ui_value = value
354 self.sa.add(hooks2)
354 self.sa.add(hooks2)
355
355
356 # enable largefiles
356 # enable largefiles
357 largefiles = RhodeCodeUi()
357 largefiles = RhodeCodeUi()
358 largefiles.ui_section = 'extensions'
358 largefiles.ui_section = 'extensions'
359 largefiles.ui_key = 'largefiles'
359 largefiles.ui_key = 'largefiles'
360 largefiles.ui_value = ''
360 largefiles.ui_value = ''
361 self.sa.add(largefiles)
361 self.sa.add(largefiles)
362
362
363 # set default largefiles cache dir, defaults to
363 # set default largefiles cache dir, defaults to
364 # /repo_store_location/.cache/largefiles
364 # /repo_store_location/.cache/largefiles
365 largefiles = RhodeCodeUi()
365 largefiles = RhodeCodeUi()
366 largefiles.ui_section = 'largefiles'
366 largefiles.ui_section = 'largefiles'
367 largefiles.ui_key = 'usercache'
367 largefiles.ui_key = 'usercache'
368 largefiles.ui_value = largefiles_store(repo_store_path)
368 largefiles.ui_value = largefiles_store(repo_store_path)
369
369
370 self.sa.add(largefiles)
370 self.sa.add(largefiles)
371
371
372 # set default lfs cache dir, defaults to
372 # set default lfs cache dir, defaults to
373 # /repo_store_location/.cache/lfs_store
373 # /repo_store_location/.cache/lfs_store
374 lfsstore = RhodeCodeUi()
374 lfsstore = RhodeCodeUi()
375 lfsstore.ui_section = 'vcs_git_lfs'
375 lfsstore.ui_section = 'vcs_git_lfs'
376 lfsstore.ui_key = 'store_location'
376 lfsstore.ui_key = 'store_location'
377 lfsstore.ui_value = lfs_store(repo_store_path)
377 lfsstore.ui_value = lfs_store(repo_store_path)
378
378
379 self.sa.add(lfsstore)
379 self.sa.add(lfsstore)
380
380
381 # enable hgsubversion disabled by default
382 hgsubversion = RhodeCodeUi()
383 hgsubversion.ui_section = 'extensions'
384 hgsubversion.ui_key = 'hgsubversion'
385 hgsubversion.ui_value = ''
386 hgsubversion.ui_active = False
387 self.sa.add(hgsubversion)
388
389 # enable hgevolve disabled by default
381 # enable hgevolve disabled by default
390 hgevolve = RhodeCodeUi()
382 hgevolve = RhodeCodeUi()
391 hgevolve.ui_section = 'extensions'
383 hgevolve.ui_section = 'extensions'
392 hgevolve.ui_key = 'evolve'
384 hgevolve.ui_key = 'evolve'
393 hgevolve.ui_value = ''
385 hgevolve.ui_value = ''
394 hgevolve.ui_active = False
386 hgevolve.ui_active = False
395 self.sa.add(hgevolve)
387 self.sa.add(hgevolve)
396
388
397 hgevolve = RhodeCodeUi()
389 hgevolve = RhodeCodeUi()
398 hgevolve.ui_section = 'experimental'
390 hgevolve.ui_section = 'experimental'
399 hgevolve.ui_key = 'evolution'
391 hgevolve.ui_key = 'evolution'
400 hgevolve.ui_value = ''
392 hgevolve.ui_value = ''
401 hgevolve.ui_active = False
393 hgevolve.ui_active = False
402 self.sa.add(hgevolve)
394 self.sa.add(hgevolve)
403
395
404 hgevolve = RhodeCodeUi()
396 hgevolve = RhodeCodeUi()
405 hgevolve.ui_section = 'experimental'
397 hgevolve.ui_section = 'experimental'
406 hgevolve.ui_key = 'evolution.exchange'
398 hgevolve.ui_key = 'evolution.exchange'
407 hgevolve.ui_value = ''
399 hgevolve.ui_value = ''
408 hgevolve.ui_active = False
400 hgevolve.ui_active = False
409 self.sa.add(hgevolve)
401 self.sa.add(hgevolve)
410
402
411 hgevolve = RhodeCodeUi()
403 hgevolve = RhodeCodeUi()
412 hgevolve.ui_section = 'extensions'
404 hgevolve.ui_section = 'extensions'
413 hgevolve.ui_key = 'topic'
405 hgevolve.ui_key = 'topic'
414 hgevolve.ui_value = ''
406 hgevolve.ui_value = ''
415 hgevolve.ui_active = False
407 hgevolve.ui_active = False
416 self.sa.add(hgevolve)
408 self.sa.add(hgevolve)
417
409
418 # enable hggit disabled by default
410 # enable hggit disabled by default
419 hggit = RhodeCodeUi()
411 hggit = RhodeCodeUi()
420 hggit.ui_section = 'extensions'
412 hggit.ui_section = 'extensions'
421 hggit.ui_key = 'hggit'
413 hggit.ui_key = 'hggit'
422 hggit.ui_value = ''
414 hggit.ui_value = ''
423 hggit.ui_active = False
415 hggit.ui_active = False
424 self.sa.add(hggit)
416 self.sa.add(hggit)
425
417
426 # set svn branch defaults
418 # set svn branch defaults
427 branches = ["/branches/*", "/trunk"]
419 branches = ["/branches/*", "/trunk"]
428 tags = ["/tags/*"]
420 tags = ["/tags/*"]
429
421
430 for branch in branches:
422 for branch in branches:
431 settings_model.create_ui_section_value(
423 settings_model.create_ui_section_value(
432 RhodeCodeUi.SVN_BRANCH_ID, branch)
424 RhodeCodeUi.SVN_BRANCH_ID, branch)
433
425
434 for tag in tags:
426 for tag in tags:
435 settings_model.create_ui_section_value(RhodeCodeUi.SVN_TAG_ID, tag)
427 settings_model.create_ui_section_value(RhodeCodeUi.SVN_TAG_ID, tag)
436
428
437 def create_auth_plugin_options(self, skip_existing=False):
429 def create_auth_plugin_options(self, skip_existing=False):
438 """
430 """
439 Create default auth plugin settings, and make it active
431 Create default auth plugin settings, and make it active
440
432
441 :param skip_existing:
433 :param skip_existing:
442 """
434 """
443 defaults = [
435 defaults = [
444 ('auth_plugins',
436 ('auth_plugins',
445 'egg:rhodecode-enterprise-ce#token,egg:rhodecode-enterprise-ce#rhodecode',
437 'egg:rhodecode-enterprise-ce#token,egg:rhodecode-enterprise-ce#rhodecode',
446 'list'),
438 'list'),
447
439
448 ('auth_authtoken_enabled',
440 ('auth_authtoken_enabled',
449 'True',
441 'True',
450 'bool'),
442 'bool'),
451
443
452 ('auth_rhodecode_enabled',
444 ('auth_rhodecode_enabled',
453 'True',
445 'True',
454 'bool'),
446 'bool'),
455 ]
447 ]
456 for k, v, t in defaults:
448 for k, v, t in defaults:
457 if (skip_existing and
449 if (skip_existing and
458 SettingsModel().get_setting_by_name(k) is not None):
450 SettingsModel().get_setting_by_name(k) is not None):
459 log.debug('Skipping option %s', k)
451 log.debug('Skipping option %s', k)
460 continue
452 continue
461 setting = RhodeCodeSetting(k, v, t)
453 setting = RhodeCodeSetting(k, v, t)
462 self.sa.add(setting)
454 self.sa.add(setting)
463
455
464 def create_default_options(self, skip_existing=False):
456 def create_default_options(self, skip_existing=False):
465 """Creates default settings"""
457 """Creates default settings"""
466
458
467 for k, v, t in [
459 for k, v, t in [
468 ('default_repo_enable_locking', False, 'bool'),
460 ('default_repo_enable_locking', False, 'bool'),
469 ('default_repo_enable_downloads', False, 'bool'),
461 ('default_repo_enable_downloads', False, 'bool'),
470 ('default_repo_enable_statistics', False, 'bool'),
462 ('default_repo_enable_statistics', False, 'bool'),
471 ('default_repo_private', False, 'bool'),
463 ('default_repo_private', False, 'bool'),
472 ('default_repo_type', 'hg', 'unicode')]:
464 ('default_repo_type', 'hg', 'unicode')]:
473
465
474 if (skip_existing and
466 if (skip_existing and
475 SettingsModel().get_setting_by_name(k) is not None):
467 SettingsModel().get_setting_by_name(k) is not None):
476 log.debug('Skipping option %s', k)
468 log.debug('Skipping option %s', k)
477 continue
469 continue
478 setting = RhodeCodeSetting(k, v, t)
470 setting = RhodeCodeSetting(k, v, t)
479 self.sa.add(setting)
471 self.sa.add(setting)
480
472
481 def fixup_groups(self):
473 def fixup_groups(self):
482 def_usr = User.get_default_user()
474 def_usr = User.get_default_user()
483 for g in RepoGroup.query().all():
475 for g in RepoGroup.query().all():
484 g.group_name = g.get_new_name(g.name)
476 g.group_name = g.get_new_name(g.name)
485 self.sa.add(g)
477 self.sa.add(g)
486 # get default perm
478 # get default perm
487 default = UserRepoGroupToPerm.query()\
479 default = UserRepoGroupToPerm.query()\
488 .filter(UserRepoGroupToPerm.group == g)\
480 .filter(UserRepoGroupToPerm.group == g)\
489 .filter(UserRepoGroupToPerm.user == def_usr)\
481 .filter(UserRepoGroupToPerm.user == def_usr)\
490 .scalar()
482 .scalar()
491
483
492 if default is None:
484 if default is None:
493 log.debug('missing default permission for group %s adding', g)
485 log.debug('missing default permission for group %s adding', g)
494 perm_obj = RepoGroupModel()._create_default_perms(g)
486 perm_obj = RepoGroupModel()._create_default_perms(g)
495 self.sa.add(perm_obj)
487 self.sa.add(perm_obj)
496
488
497 def reset_permissions(self, username):
489 def reset_permissions(self, username):
498 """
490 """
499 Resets permissions to default state, useful when old systems had
491 Resets permissions to default state, useful when old systems had
500 bad permissions, we must clean them up
492 bad permissions, we must clean them up
501
493
502 :param username:
494 :param username:
503 """
495 """
504 default_user = User.get_by_username(username)
496 default_user = User.get_by_username(username)
505 if not default_user:
497 if not default_user:
506 return
498 return
507
499
508 u2p = UserToPerm.query()\
500 u2p = UserToPerm.query()\
509 .filter(UserToPerm.user == default_user).all()
501 .filter(UserToPerm.user == default_user).all()
510 fixed = False
502 fixed = False
511 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
503 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
512 for p in u2p:
504 for p in u2p:
513 Session().delete(p)
505 Session().delete(p)
514 fixed = True
506 fixed = True
515 self.populate_default_permissions()
507 self.populate_default_permissions()
516 return fixed
508 return fixed
517
509
518 def config_prompt(self, test_repo_path='', retries=3):
510 def config_prompt(self, test_repo_path='', retries=3):
519 defaults = self.cli_args
511 defaults = self.cli_args
520 _path = defaults.get('repos_location')
512 _path = defaults.get('repos_location')
521 if retries == 3:
513 if retries == 3:
522 log.info('Setting up repositories config')
514 log.info('Setting up repositories config')
523
515
524 if _path is not None:
516 if _path is not None:
525 path = _path
517 path = _path
526 elif not self.tests and not test_repo_path:
518 elif not self.tests and not test_repo_path:
527 path = input(
519 path = input(
528 'Enter a valid absolute path to store repositories. '
520 'Enter a valid absolute path to store repositories. '
529 'All repositories in that path will be added automatically:'
521 'All repositories in that path will be added automatically:'
530 )
522 )
531 else:
523 else:
532 path = test_repo_path
524 path = test_repo_path
533 path_ok = True
525 path_ok = True
534
526
535 # check proper dir
527 # check proper dir
536 if not os.path.isdir(path):
528 if not os.path.isdir(path):
537 path_ok = False
529 path_ok = False
538 log.error('Given path %s is not a valid directory', path)
530 log.error('Given path %s is not a valid directory', path)
539
531
540 elif not os.path.isabs(path):
532 elif not os.path.isabs(path):
541 path_ok = False
533 path_ok = False
542 log.error('Given path %s is not an absolute path', path)
534 log.error('Given path %s is not an absolute path', path)
543
535
544 # check if path is at least readable.
536 # check if path is at least readable.
545 if not os.access(path, os.R_OK):
537 if not os.access(path, os.R_OK):
546 path_ok = False
538 path_ok = False
547 log.error('Given path %s is not readable', path)
539 log.error('Given path %s is not readable', path)
548
540
549 # check write access, warn user about non writeable paths
541 # check write access, warn user about non writeable paths
550 elif not os.access(path, os.W_OK) and path_ok:
542 elif not os.access(path, os.W_OK) and path_ok:
551 log.warning('No write permission to given path %s', path)
543 log.warning('No write permission to given path %s', path)
552
544
553 q = (f'Given path {path} is not writeable, do you want to '
545 q = (f'Given path {path} is not writeable, do you want to '
554 f'continue with read only mode ? [y/n]')
546 f'continue with read only mode ? [y/n]')
555 if not self.ask_ok(q):
547 if not self.ask_ok(q):
556 log.error('Canceled by user')
548 log.error('Canceled by user')
557 sys.exit(-1)
549 sys.exit(-1)
558
550
559 if retries == 0:
551 if retries == 0:
560 sys.exit('max retries reached')
552 sys.exit('max retries reached')
561 if not path_ok:
553 if not path_ok:
562 retries -= 1
554 retries -= 1
563 return self.config_prompt(test_repo_path, retries)
555 return self.config_prompt(test_repo_path, retries)
564
556
565 real_path = os.path.normpath(os.path.realpath(path))
557 real_path = os.path.normpath(os.path.realpath(path))
566
558
567 if real_path != os.path.normpath(path):
559 if real_path != os.path.normpath(path):
568 q = (f'Path looks like a symlink, RhodeCode Enterprise will store '
560 q = (f'Path looks like a symlink, RhodeCode Enterprise will store '
569 f'given path as {real_path} ? [y/n]')
561 f'given path as {real_path} ? [y/n]')
570 if not self.ask_ok(q):
562 if not self.ask_ok(q):
571 log.error('Canceled by user')
563 log.error('Canceled by user')
572 sys.exit(-1)
564 sys.exit(-1)
573
565
574 return real_path
566 return real_path
575
567
576 def create_settings(self, path):
568 def create_settings(self, path):
577
569
578 self.create_ui_settings(path)
570 self.create_ui_settings(path)
579
571
580 ui_config = [
572 ui_config = [
581 ('web', 'push_ssl', 'False'),
573 ('web', 'push_ssl', 'False'),
582 ('web', 'allow_archive', 'gz zip bz2'),
574 ('web', 'allow_archive', 'gz zip bz2'),
583 ('web', 'allow_push', '*'),
575 ('web', 'allow_push', '*'),
584 ('web', 'baseurl', '/'),
576 ('web', 'baseurl', '/'),
585 ('paths', '/', path),
577 ('paths', '/', path),
586 ('phases', 'publish', 'True')
578 ('phases', 'publish', 'True')
587 ]
579 ]
588 for section, key, value in ui_config:
580 for section, key, value in ui_config:
589 ui_conf = RhodeCodeUi()
581 ui_conf = RhodeCodeUi()
590 setattr(ui_conf, 'ui_section', section)
582 setattr(ui_conf, 'ui_section', section)
591 setattr(ui_conf, 'ui_key', key)
583 setattr(ui_conf, 'ui_key', key)
592 setattr(ui_conf, 'ui_value', value)
584 setattr(ui_conf, 'ui_value', value)
593 self.sa.add(ui_conf)
585 self.sa.add(ui_conf)
594
586
595 # rhodecode app settings
587 # rhodecode app settings
596 settings = [
588 settings = [
597 ('realm', 'RhodeCode', 'unicode'),
589 ('realm', 'RhodeCode', 'unicode'),
598 ('title', '', 'unicode'),
590 ('title', '', 'unicode'),
599 ('pre_code', '', 'unicode'),
591 ('pre_code', '', 'unicode'),
600 ('post_code', '', 'unicode'),
592 ('post_code', '', 'unicode'),
601
593
602 # Visual
594 # Visual
603 ('show_public_icon', True, 'bool'),
595 ('show_public_icon', True, 'bool'),
604 ('show_private_icon', True, 'bool'),
596 ('show_private_icon', True, 'bool'),
605 ('stylify_metatags', True, 'bool'),
597 ('stylify_metatags', True, 'bool'),
606 ('dashboard_items', 100, 'int'),
598 ('dashboard_items', 100, 'int'),
607 ('admin_grid_items', 25, 'int'),
599 ('admin_grid_items', 25, 'int'),
608
600
609 ('markup_renderer', 'markdown', 'unicode'),
601 ('markup_renderer', 'markdown', 'unicode'),
610
602
611 ('repository_fields', True, 'bool'),
603 ('repository_fields', True, 'bool'),
612 ('show_version', True, 'bool'),
604 ('show_version', True, 'bool'),
613 ('show_revision_number', True, 'bool'),
605 ('show_revision_number', True, 'bool'),
614 ('show_sha_length', 12, 'int'),
606 ('show_sha_length', 12, 'int'),
615
607
616 ('use_gravatar', False, 'bool'),
608 ('use_gravatar', False, 'bool'),
617 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
609 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
618
610
619 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
611 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
620 ('clone_uri_id_tmpl', Repository.DEFAULT_CLONE_URI_ID, 'unicode'),
612 ('clone_uri_id_tmpl', Repository.DEFAULT_CLONE_URI_ID, 'unicode'),
621 ('clone_uri_ssh_tmpl', Repository.DEFAULT_CLONE_URI_SSH, 'unicode'),
613 ('clone_uri_ssh_tmpl', Repository.DEFAULT_CLONE_URI_SSH, 'unicode'),
622 ('support_url', '', 'unicode'),
614 ('support_url', '', 'unicode'),
623 ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
615 ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
624
616
625 # VCS Settings
617 # VCS Settings
626 ('pr_merge_enabled', True, 'bool'),
618 ('pr_merge_enabled', True, 'bool'),
627 ('use_outdated_comments', True, 'bool'),
619 ('use_outdated_comments', True, 'bool'),
628 ('diff_cache', True, 'bool'),
620 ('diff_cache', True, 'bool'),
629 ]
621 ]
630
622
631 for key, val, type_ in settings:
623 for key, val, type_ in settings:
632 sett = RhodeCodeSetting(key, val, type_)
624 sett = RhodeCodeSetting(key, val, type_)
633 self.sa.add(sett)
625 self.sa.add(sett)
634
626
635 self.create_auth_plugin_options()
627 self.create_auth_plugin_options()
636 self.create_default_options()
628 self.create_default_options()
637
629
638 log.info('created ui config')
630 log.info('created ui config')
639
631
640 def create_user(self, username, password, email='', admin=False,
632 def create_user(self, username, password, email='', admin=False,
641 strict_creation_check=True, api_key=None):
633 strict_creation_check=True, api_key=None):
642 log.info('creating user `%s`', username)
634 log.info('creating user `%s`', username)
643 user = UserModel().create_or_update(
635 user = UserModel().create_or_update(
644 username, password, email, firstname='RhodeCode', lastname='Admin',
636 username, password, email, firstname='RhodeCode', lastname='Admin',
645 active=True, admin=admin, extern_type="rhodecode",
637 active=True, admin=admin, extern_type="rhodecode",
646 strict_creation_check=strict_creation_check)
638 strict_creation_check=strict_creation_check)
647
639
648 if api_key:
640 if api_key:
649 log.info('setting a new default auth token for user `%s`', username)
641 log.info('setting a new default auth token for user `%s`', username)
650 UserModel().add_auth_token(
642 UserModel().add_auth_token(
651 user=user, lifetime_minutes=-1,
643 user=user, lifetime_minutes=-1,
652 role=UserModel.auth_token_role.ROLE_ALL,
644 role=UserModel.auth_token_role.ROLE_ALL,
653 description='BUILTIN TOKEN')
645 description='BUILTIN TOKEN')
654
646
655 def create_default_user(self):
647 def create_default_user(self):
656 log.info('creating default user')
648 log.info('creating default user')
657 # create default user for handling default permissions.
649 # create default user for handling default permissions.
658 user = UserModel().create_or_update(username=User.DEFAULT_USER,
650 user = UserModel().create_or_update(username=User.DEFAULT_USER,
659 password=str(uuid.uuid1())[:20],
651 password=str(uuid.uuid1())[:20],
660 email=User.DEFAULT_USER_EMAIL,
652 email=User.DEFAULT_USER_EMAIL,
661 firstname='Anonymous',
653 firstname='Anonymous',
662 lastname='User',
654 lastname='User',
663 strict_creation_check=False)
655 strict_creation_check=False)
664 # based on configuration options activate/de-activate this user which
656 # based on configuration options activate/de-activate this user which
665 # controls anonymous access
657 # controls anonymous access
666 if self.cli_args.get('public_access') is False:
658 if self.cli_args.get('public_access') is False:
667 log.info('Public access disabled')
659 log.info('Public access disabled')
668 user.active = False
660 user.active = False
669 Session().add(user)
661 Session().add(user)
670 Session().commit()
662 Session().commit()
671
663
672 def create_permissions(self):
664 def create_permissions(self):
673 """
665 """
674 Creates all permissions defined in the system
666 Creates all permissions defined in the system
675 """
667 """
676 # module.(access|create|change|delete)_[name]
668 # module.(access|create|change|delete)_[name]
677 # module.(none|read|write|admin)
669 # module.(none|read|write|admin)
678 log.info('creating permissions')
670 log.info('creating permissions')
679 PermissionModel(self.sa).create_permissions()
671 PermissionModel(self.sa).create_permissions()
680
672
681 def populate_default_permissions(self):
673 def populate_default_permissions(self):
682 """
674 """
683 Populate default permissions. It will create only the default
675 Populate default permissions. It will create only the default
684 permissions that are missing, and not alter already defined ones
676 permissions that are missing, and not alter already defined ones
685 """
677 """
686 log.info('creating default user permissions')
678 log.info('creating default user permissions')
687 PermissionModel(self.sa).create_default_user_permissions(user=User.DEFAULT_USER)
679 PermissionModel(self.sa).create_default_user_permissions(user=User.DEFAULT_USER)
@@ -1,1983 +1,1988 b''
1 # Copyright (C) 2014-2023 RhodeCode GmbH
1 # Copyright (C) 2014-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 Base module for all VCS systems
20 Base module for all VCS systems
21 """
21 """
22 import os
22 import os
23 import re
23 import re
24 import time
24 import time
25 import shutil
25 import shutil
26 import datetime
26 import datetime
27 import fnmatch
27 import fnmatch
28 import itertools
28 import itertools
29 import logging
29 import logging
30 import dataclasses
30 import dataclasses
31 import warnings
31 import warnings
32
32
33 from zope.cachedescriptors.property import Lazy as LazyProperty
33 from zope.cachedescriptors.property import Lazy as LazyProperty
34
34
35
35
36 import rhodecode
36 import rhodecode
37 from rhodecode.translation import lazy_ugettext
37 from rhodecode.translation import lazy_ugettext
38 from rhodecode.lib.utils2 import safe_str, CachedProperty
38 from rhodecode.lib.utils2 import safe_str, CachedProperty
39 from rhodecode.lib.vcs.utils import author_name, author_email
39 from rhodecode.lib.vcs.utils import author_name, author_email
40 from rhodecode.lib.vcs.conf import settings
40 from rhodecode.lib.vcs.conf import settings
41 from rhodecode.lib.vcs.exceptions import (
41 from rhodecode.lib.vcs.exceptions import (
42 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
42 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
43 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
43 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
44 NodeDoesNotExistError, NodeNotChangedError, VCSError,
44 NodeDoesNotExistError, NodeNotChangedError, VCSError,
45 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
45 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
46 RepositoryError)
46 RepositoryError)
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 FILEMODE_DEFAULT = 0o100644
52 FILEMODE_DEFAULT = 0o100644
53 FILEMODE_EXECUTABLE = 0o100755
53 FILEMODE_EXECUTABLE = 0o100755
54 EMPTY_COMMIT_ID = '0' * 40
54 EMPTY_COMMIT_ID = '0' * 40
55
55
56
56
57 @dataclasses.dataclass
57 @dataclasses.dataclass
58 class Reference:
58 class Reference:
59 type: str
59 type: str
60 name: str
60 name: str
61 commit_id: str
61 commit_id: str
62
62
63 def __iter__(self):
63 def __iter__(self):
64 yield self.type
64 yield self.type
65 yield self.name
65 yield self.name
66 yield self.commit_id
66 yield self.commit_id
67
67
68 @property
68 @property
69 def branch(self):
69 def branch(self):
70 if self.type == 'branch':
70 if self.type == 'branch':
71 return self.name
71 return self.name
72
72
73 @property
73 @property
74 def bookmark(self):
74 def bookmark(self):
75 if self.type == 'book':
75 if self.type == 'book':
76 return self.name
76 return self.name
77
77
78 @property
78 @property
79 def to_str(self):
79 def to_str(self):
80 return reference_to_unicode(self)
80 return reference_to_unicode(self)
81
81
82 def asdict(self):
82 def asdict(self):
83 return dict(
83 return dict(
84 type=self.type,
84 type=self.type,
85 name=self.name,
85 name=self.name,
86 commit_id=self.commit_id
86 commit_id=self.commit_id
87 )
87 )
88
88
89
89
90 def unicode_to_reference(raw: str):
90 def unicode_to_reference(raw: str):
91 """
91 """
92 Convert a unicode (or string) to a reference object.
92 Convert a unicode (or string) to a reference object.
93 If unicode evaluates to False it returns None.
93 If unicode evaluates to False it returns None.
94 """
94 """
95 if raw:
95 if raw:
96 refs = raw.split(':')
96 refs = raw.split(':')
97 return Reference(*refs)
97 return Reference(*refs)
98 else:
98 else:
99 return None
99 return None
100
100
101
101
102 def reference_to_unicode(ref: Reference):
102 def reference_to_unicode(ref: Reference):
103 """
103 """
104 Convert a reference object to unicode.
104 Convert a reference object to unicode.
105 If reference is None it returns None.
105 If reference is None it returns None.
106 """
106 """
107 if ref:
107 if ref:
108 return ':'.join(ref)
108 return ':'.join(ref)
109 else:
109 else:
110 return None
110 return None
111
111
112
112
113 class MergeFailureReason(object):
113 class MergeFailureReason(object):
114 """
114 """
115 Enumeration with all the reasons why the server side merge could fail.
115 Enumeration with all the reasons why the server side merge could fail.
116
116
117 DO NOT change the number of the reasons, as they may be stored in the
117 DO NOT change the number of the reasons, as they may be stored in the
118 database.
118 database.
119
119
120 Changing the name of a reason is acceptable and encouraged to deprecate old
120 Changing the name of a reason is acceptable and encouraged to deprecate old
121 reasons.
121 reasons.
122 """
122 """
123
123
124 # Everything went well.
124 # Everything went well.
125 NONE = 0
125 NONE = 0
126
126
127 # An unexpected exception was raised. Check the logs for more details.
127 # An unexpected exception was raised. Check the logs for more details.
128 UNKNOWN = 1
128 UNKNOWN = 1
129
129
130 # The merge was not successful, there are conflicts.
130 # The merge was not successful, there are conflicts.
131 MERGE_FAILED = 2
131 MERGE_FAILED = 2
132
132
133 # The merge succeeded but we could not push it to the target repository.
133 # The merge succeeded but we could not push it to the target repository.
134 PUSH_FAILED = 3
134 PUSH_FAILED = 3
135
135
136 # The specified target is not a head in the target repository.
136 # The specified target is not a head in the target repository.
137 TARGET_IS_NOT_HEAD = 4
137 TARGET_IS_NOT_HEAD = 4
138
138
139 # The source repository contains more branches than the target. Pushing
139 # The source repository contains more branches than the target. Pushing
140 # the merge will create additional branches in the target.
140 # the merge will create additional branches in the target.
141 HG_SOURCE_HAS_MORE_BRANCHES = 5
141 HG_SOURCE_HAS_MORE_BRANCHES = 5
142
142
143 # The target reference has multiple heads. That does not allow to correctly
143 # The target reference has multiple heads. That does not allow to correctly
144 # identify the target location. This could only happen for mercurial
144 # identify the target location. This could only happen for mercurial
145 # branches.
145 # branches.
146 HG_TARGET_HAS_MULTIPLE_HEADS = 6
146 HG_TARGET_HAS_MULTIPLE_HEADS = 6
147
147
148 # The target repository is locked
148 # The target repository is locked
149 TARGET_IS_LOCKED = 7
149 TARGET_IS_LOCKED = 7
150
150
151 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
151 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
152 # A involved commit could not be found.
152 # A involved commit could not be found.
153 _DEPRECATED_MISSING_COMMIT = 8
153 _DEPRECATED_MISSING_COMMIT = 8
154
154
155 # The target repo reference is missing.
155 # The target repo reference is missing.
156 MISSING_TARGET_REF = 9
156 MISSING_TARGET_REF = 9
157
157
158 # The source repo reference is missing.
158 # The source repo reference is missing.
159 MISSING_SOURCE_REF = 10
159 MISSING_SOURCE_REF = 10
160
160
161 # The merge was not successful, there are conflicts related to sub
161 # The merge was not successful, there are conflicts related to sub
162 # repositories.
162 # repositories.
163 SUBREPO_MERGE_FAILED = 11
163 SUBREPO_MERGE_FAILED = 11
164
164
165
165
166 class UpdateFailureReason(object):
166 class UpdateFailureReason(object):
167 """
167 """
168 Enumeration with all the reasons why the pull request update could fail.
168 Enumeration with all the reasons why the pull request update could fail.
169
169
170 DO NOT change the number of the reasons, as they may be stored in the
170 DO NOT change the number of the reasons, as they may be stored in the
171 database.
171 database.
172
172
173 Changing the name of a reason is acceptable and encouraged to deprecate old
173 Changing the name of a reason is acceptable and encouraged to deprecate old
174 reasons.
174 reasons.
175 """
175 """
176
176
177 # Everything went well.
177 # Everything went well.
178 NONE = 0
178 NONE = 0
179
179
180 # An unexpected exception was raised. Check the logs for more details.
180 # An unexpected exception was raised. Check the logs for more details.
181 UNKNOWN = 1
181 UNKNOWN = 1
182
182
183 # The pull request is up to date.
183 # The pull request is up to date.
184 NO_CHANGE = 2
184 NO_CHANGE = 2
185
185
186 # The pull request has a reference type that is not supported for update.
186 # The pull request has a reference type that is not supported for update.
187 WRONG_REF_TYPE = 3
187 WRONG_REF_TYPE = 3
188
188
189 # Update failed because the target reference is missing.
189 # Update failed because the target reference is missing.
190 MISSING_TARGET_REF = 4
190 MISSING_TARGET_REF = 4
191
191
192 # Update failed because the source reference is missing.
192 # Update failed because the source reference is missing.
193 MISSING_SOURCE_REF = 5
193 MISSING_SOURCE_REF = 5
194
194
195
195
196 class MergeResponse(object):
196 class MergeResponse(object):
197
197
198 # uses .format(**metadata) for variables
198 # uses .format(**metadata) for variables
199 MERGE_STATUS_MESSAGES = {
199 MERGE_STATUS_MESSAGES = {
200 MergeFailureReason.NONE: lazy_ugettext(
200 MergeFailureReason.NONE: lazy_ugettext(
201 'This pull request can be automatically merged.'),
201 'This pull request can be automatically merged.'),
202 MergeFailureReason.UNKNOWN: lazy_ugettext(
202 MergeFailureReason.UNKNOWN: lazy_ugettext(
203 'This pull request cannot be merged because of an unhandled exception. '
203 'This pull request cannot be merged because of an unhandled exception. '
204 '{exception}'),
204 '{exception}'),
205 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
205 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
206 'This pull request cannot be merged because of merge conflicts. {unresolved_files}'),
206 'This pull request cannot be merged because of merge conflicts. {unresolved_files}'),
207 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
207 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
208 'This pull request could not be merged because push to '
208 'This pull request could not be merged because push to '
209 'target:`{target}@{merge_commit}` failed.'),
209 'target:`{target}@{merge_commit}` failed.'),
210 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
210 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
211 'This pull request cannot be merged because the target '
211 'This pull request cannot be merged because the target '
212 '`{target_ref.name}` is not a head.'),
212 '`{target_ref.name}` is not a head.'),
213 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
213 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
214 'This pull request cannot be merged because the source contains '
214 'This pull request cannot be merged because the source contains '
215 'more branches than the target.'),
215 'more branches than the target.'),
216 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
216 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
217 'This pull request cannot be merged because the target `{target_ref.name}` '
217 'This pull request cannot be merged because the target `{target_ref.name}` '
218 'has multiple heads: `{heads}`.'),
218 'has multiple heads: `{heads}`.'),
219 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
219 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
220 'This pull request cannot be merged because the target repository is '
220 'This pull request cannot be merged because the target repository is '
221 'locked by {locked_by}.'),
221 'locked by {locked_by}.'),
222
222
223 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
223 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
224 'This pull request cannot be merged because the target '
224 'This pull request cannot be merged because the target '
225 'reference `{target_ref.name}` is missing.'),
225 'reference `{target_ref.name}` is missing.'),
226 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
226 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
227 'This pull request cannot be merged because the source '
227 'This pull request cannot be merged because the source '
228 'reference `{source_ref.name}` is missing.'),
228 'reference `{source_ref.name}` is missing.'),
229 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
229 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
230 'This pull request cannot be merged because of conflicts related '
230 'This pull request cannot be merged because of conflicts related '
231 'to sub repositories.'),
231 'to sub repositories.'),
232
232
233 # Deprecations
233 # Deprecations
234 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
234 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
235 'This pull request cannot be merged because the target or the '
235 'This pull request cannot be merged because the target or the '
236 'source reference is missing.'),
236 'source reference is missing.'),
237
237
238 }
238 }
239
239
240 def __init__(self, possible, executed, merge_ref: Reference, failure_reason, metadata=None):
240 def __init__(self, possible, executed, merge_ref: Reference, failure_reason, metadata=None):
241 self.possible = possible
241 self.possible = possible
242 self.executed = executed
242 self.executed = executed
243 self.merge_ref = merge_ref
243 self.merge_ref = merge_ref
244 self.failure_reason = failure_reason
244 self.failure_reason = failure_reason
245 self.metadata = metadata or {}
245 self.metadata = metadata or {}
246
246
247 def __repr__(self):
247 def __repr__(self):
248 return f'<MergeResponse:{self.label} {self.failure_reason}>'
248 return f'<MergeResponse:{self.label} {self.failure_reason}>'
249
249
250 def __eq__(self, other):
250 def __eq__(self, other):
251 same_instance = isinstance(other, self.__class__)
251 same_instance = isinstance(other, self.__class__)
252 return same_instance \
252 return same_instance \
253 and self.possible == other.possible \
253 and self.possible == other.possible \
254 and self.executed == other.executed \
254 and self.executed == other.executed \
255 and self.failure_reason == other.failure_reason
255 and self.failure_reason == other.failure_reason
256
256
257 @property
257 @property
258 def label(self):
258 def label(self):
259 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
259 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
260 not k.startswith('_'))
260 not k.startswith('_'))
261 return label_dict.get(self.failure_reason)
261 return label_dict.get(self.failure_reason)
262
262
263 @property
263 @property
264 def merge_status_message(self):
264 def merge_status_message(self):
265 """
265 """
266 Return a human friendly error message for the given merge status code.
266 Return a human friendly error message for the given merge status code.
267 """
267 """
268 msg = safe_str(self.MERGE_STATUS_MESSAGES[self.failure_reason])
268 msg = safe_str(self.MERGE_STATUS_MESSAGES[self.failure_reason])
269
269
270 try:
270 try:
271 return msg.format(**self.metadata)
271 return msg.format(**self.metadata)
272 except Exception:
272 except Exception:
273 log.exception('Failed to format %s message', self)
273 log.exception('Failed to format %s message', self)
274 return msg
274 return msg
275
275
276 def asdict(self):
276 def asdict(self):
277 data = {}
277 data = {}
278 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
278 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
279 'merge_status_message']:
279 'merge_status_message']:
280 data[k] = getattr(self, k)
280 data[k] = getattr(self, k)
281 return data
281 return data
282
282
283
283
284 class TargetRefMissing(ValueError):
284 class TargetRefMissing(ValueError):
285 pass
285 pass
286
286
287
287
288 class SourceRefMissing(ValueError):
288 class SourceRefMissing(ValueError):
289 pass
289 pass
290
290
291
291
292 class BaseRepository(object):
292 class BaseRepository(object):
293 """
293 """
294 Base Repository for final backends
294 Base Repository for final backends
295
295
296 .. attribute:: DEFAULT_BRANCH_NAME
296 .. attribute:: DEFAULT_BRANCH_NAME
297
297
298 name of default branch (i.e. "trunk" for svn, "master" for git etc.
298 name of default branch (i.e. "trunk" for svn, "master" for git etc.
299
299
300 .. attribute:: commit_ids
300 .. attribute:: commit_ids
301
301
302 list of all available commit ids, in ascending order
302 list of all available commit ids, in ascending order
303
303
304 .. attribute:: path
304 .. attribute:: path
305
305
306 absolute path to the repository
306 absolute path to the repository
307
307
308 .. attribute:: bookmarks
308 .. attribute:: bookmarks
309
309
310 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
310 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
311 there are no bookmarks or the backend implementation does not support
311 there are no bookmarks or the backend implementation does not support
312 bookmarks.
312 bookmarks.
313
313
314 .. attribute:: tags
314 .. attribute:: tags
315
315
316 Mapping from name to :term:`Commit ID` of the tag.
316 Mapping from name to :term:`Commit ID` of the tag.
317
317
318 """
318 """
319
319
320 DEFAULT_BRANCH_NAME = None
320 DEFAULT_BRANCH_NAME = None
321 DEFAULT_CONTACT = "Unknown"
321 DEFAULT_CONTACT = "Unknown"
322 DEFAULT_DESCRIPTION = "unknown"
322 DEFAULT_DESCRIPTION = "unknown"
323 EMPTY_COMMIT_ID = '0' * 40
323 EMPTY_COMMIT_ID = '0' * 40
324 COMMIT_ID_PAT = re.compile(r'[0-9a-fA-F]{40}')
324 COMMIT_ID_PAT = re.compile(r'[0-9a-fA-F]{40}')
325
325
326 path = None
326 path = None
327
327
328 _is_empty = None
328 _is_empty = None
329 _commit_ids = {}
329 _commit_ids = {}
330
330
331 def __init__(self, repo_path, config=None, create=False, **kwargs):
331 def __init__(self, repo_path, config=None, create=False, **kwargs):
332 """
332 """
333 Initializes repository. Raises RepositoryError if repository could
333 Initializes repository. Raises RepositoryError if repository could
334 not be find at the given ``repo_path`` or directory at ``repo_path``
334 not be find at the given ``repo_path`` or directory at ``repo_path``
335 exists and ``create`` is set to True.
335 exists and ``create`` is set to True.
336
336
337 :param repo_path: local path of the repository
337 :param repo_path: local path of the repository
338 :param config: repository configuration
338 :param config: repository configuration
339 :param create=False: if set to True, would try to create repository.
339 :param create=False: if set to True, would try to create repository.
340 :param src_url=None: if set, should be proper url from which repository
340 :param src_url=None: if set, should be proper url from which repository
341 would be cloned; requires ``create`` parameter to be set to True -
341 would be cloned; requires ``create`` parameter to be set to True -
342 raises RepositoryError if src_url is set and create evaluates to
342 raises RepositoryError if src_url is set and create evaluates to
343 False
343 False
344 """
344 """
345 raise NotImplementedError
345 raise NotImplementedError
346
346
347 def __repr__(self):
347 def __repr__(self):
348 return f'<{self.__class__.__name__} at {self.path}>'
348 return f'<{self.__class__.__name__} at {self.path}>'
349
349
350 def __len__(self):
350 def __len__(self):
351 return self.count()
351 return self.count()
352
352
353 def __eq__(self, other):
353 def __eq__(self, other):
354 same_instance = isinstance(other, self.__class__)
354 same_instance = isinstance(other, self.__class__)
355 return same_instance and other.path == self.path
355 return same_instance and other.path == self.path
356
356
357 def __ne__(self, other):
357 def __ne__(self, other):
358 return not self.__eq__(other)
358 return not self.__eq__(other)
359
359
360 def get_create_shadow_cache_pr_path(self, db_repo):
360 def get_create_shadow_cache_pr_path(self, db_repo):
361 path = db_repo.cached_diffs_dir
361 path = db_repo.cached_diffs_dir
362 if not os.path.exists(path):
362 if not os.path.exists(path):
363 os.makedirs(path, 0o755)
363 os.makedirs(path, 0o755)
364 return path
364 return path
365
365
366 @classmethod
366 @classmethod
367 def get_default_config(cls, default=None):
367 def get_default_config(cls, default=None):
368 config = Config()
368 config = Config()
369 if default and isinstance(default, list):
369 if default and isinstance(default, list):
370 for section, key, val in default:
370 for section, key, val in default:
371 config.set(section, key, val)
371 config.set(section, key, val)
372 return config
372 return config
373
373
374 @LazyProperty
374 @LazyProperty
375 def _remote(self):
375 def _remote(self):
376 raise NotImplementedError
376 raise NotImplementedError
377
377
378 def _heads(self, branch=None):
378 def _heads(self, branch=None):
379 return []
379 return []
380
380
381 @LazyProperty
381 @LazyProperty
382 def EMPTY_COMMIT(self):
382 def EMPTY_COMMIT(self):
383 return EmptyCommit(self.EMPTY_COMMIT_ID)
383 return EmptyCommit(self.EMPTY_COMMIT_ID)
384
384
385 @LazyProperty
385 @LazyProperty
386 def alias(self):
386 def alias(self):
387 for k, v in settings.BACKENDS.items():
387 for k, v in settings.BACKENDS.items():
388 if v.split('.')[-1] == str(self.__class__.__name__):
388 if v.split('.')[-1] == str(self.__class__.__name__):
389 return k
389 return k
390
390
391 @LazyProperty
391 @LazyProperty
392 def name(self):
392 def name(self):
393 return safe_str(os.path.basename(self.path))
393 return safe_str(os.path.basename(self.path))
394
394
395 @LazyProperty
395 @LazyProperty
396 def description(self):
396 def description(self):
397 raise NotImplementedError
397 raise NotImplementedError
398
398
399 def refs(self):
399 def refs(self):
400 """
400 """
401 returns a `dict` with branches, bookmarks, tags, and closed_branches
401 returns a `dict` with branches, bookmarks, tags, and closed_branches
402 for this repository
402 for this repository
403 """
403 """
404 return dict(
404 return dict(
405 branches=self.branches,
405 branches=self.branches,
406 branches_closed=self.branches_closed,
406 branches_closed=self.branches_closed,
407 tags=self.tags,
407 tags=self.tags,
408 bookmarks=self.bookmarks
408 bookmarks=self.bookmarks
409 )
409 )
410
410
411 @LazyProperty
411 @LazyProperty
412 def branches(self):
412 def branches(self):
413 """
413 """
414 A `dict` which maps branch names to commit ids.
414 A `dict` which maps branch names to commit ids.
415 """
415 """
416 raise NotImplementedError
416 raise NotImplementedError
417
417
418 @LazyProperty
418 @LazyProperty
419 def branches_closed(self):
419 def branches_closed(self):
420 """
420 """
421 A `dict` which maps tags names to commit ids.
421 A `dict` which maps tags names to commit ids.
422 """
422 """
423 raise NotImplementedError
423 raise NotImplementedError
424
424
425 @LazyProperty
425 @LazyProperty
426 def bookmarks(self):
426 def bookmarks(self):
427 """
427 """
428 A `dict` which maps tags names to commit ids.
428 A `dict` which maps tags names to commit ids.
429 """
429 """
430 raise NotImplementedError
430 raise NotImplementedError
431
431
432 @LazyProperty
432 @LazyProperty
433 def tags(self):
433 def tags(self):
434 """
434 """
435 A `dict` which maps tags names to commit ids.
435 A `dict` which maps tags names to commit ids.
436 """
436 """
437 raise NotImplementedError
437 raise NotImplementedError
438
438
439 @LazyProperty
439 @LazyProperty
440 def size(self):
440 def size(self):
441 """
441 """
442 Returns combined size in bytes for all repository files
442 Returns combined size in bytes for all repository files
443 """
443 """
444 tip = self.get_commit()
444 tip = self.get_commit()
445 return tip.size
445 return tip.size
446
446
447 def size_at_commit(self, commit_id):
447 def size_at_commit(self, commit_id):
448 commit = self.get_commit(commit_id)
448 commit = self.get_commit(commit_id)
449 return commit.size
449 return commit.size
450
450
451 def _check_for_empty(self):
451 def _check_for_empty(self):
452 no_commits = len(self._commit_ids) == 0
452 no_commits = len(self._commit_ids) == 0
453 if no_commits:
453 if no_commits:
454 # check on remote to be sure
454 # check on remote to be sure
455 return self._remote.is_empty()
455 return self._remote.is_empty()
456 else:
456 else:
457 return False
457 return False
458
458
459 def is_empty(self):
459 def is_empty(self):
460 if rhodecode.is_test:
460 if rhodecode.is_test:
461 return self._check_for_empty()
461 return self._check_for_empty()
462
462
463 if self._is_empty is None:
463 if self._is_empty is None:
464 # cache empty for production, but not tests
464 # cache empty for production, but not tests
465 self._is_empty = self._check_for_empty()
465 self._is_empty = self._check_for_empty()
466
466
467 return self._is_empty
467 return self._is_empty
468
468
469 @staticmethod
469 @staticmethod
470 def check_url(url, config):
470 def check_url(url, config):
471 """
471 """
472 Function will check given url and try to verify if it's a valid
472 Function will check given url and try to verify if it's a valid
473 link.
473 link.
474 """
474 """
475 raise NotImplementedError
475 raise NotImplementedError
476
476
477 @staticmethod
477 @staticmethod
478 def is_valid_repository(path):
478 def is_valid_repository(path):
479 """
479 """
480 Check if given `path` contains a valid repository of this backend
480 Check if given `path` contains a valid repository of this backend
481 """
481 """
482 raise NotImplementedError
482 raise NotImplementedError
483
483
484 # ==========================================================================
484 # ==========================================================================
485 # COMMITS
485 # COMMITS
486 # ==========================================================================
486 # ==========================================================================
487
487
488 @CachedProperty
488 @CachedProperty
489 def commit_ids(self):
489 def commit_ids(self):
490 raise NotImplementedError
490 raise NotImplementedError
491
491
492 def append_commit_id(self, commit_id):
492 def append_commit_id(self, commit_id):
493 if commit_id not in self.commit_ids:
493 if commit_id not in self.commit_ids:
494 self._rebuild_cache(self.commit_ids + [commit_id])
494 self._rebuild_cache(self.commit_ids + [commit_id])
495
495
496 # clear cache
496 # clear cache
497 self._invalidate_prop_cache('commit_ids')
497 self._invalidate_prop_cache('commit_ids')
498 self._is_empty = False
498 self._is_empty = False
499
499
500 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
500 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
501 translate_tag=None, maybe_unreachable=False, reference_obj=None):
501 translate_tag=None, maybe_unreachable=False, reference_obj=None):
502 """
502 """
503 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
503 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
504 are both None, most recent commit is returned.
504 are both None, most recent commit is returned.
505
505
506 :param pre_load: Optional. List of commit attributes to load.
506 :param pre_load: Optional. List of commit attributes to load.
507
507
508 :raises ``EmptyRepositoryError``: if there are no commits
508 :raises ``EmptyRepositoryError``: if there are no commits
509 """
509 """
510 raise NotImplementedError
510 raise NotImplementedError
511
511
512 def __iter__(self):
512 def __iter__(self):
513 for commit_id in self.commit_ids:
513 for commit_id in self.commit_ids:
514 yield self.get_commit(commit_id=commit_id)
514 yield self.get_commit(commit_id=commit_id)
515
515
516 def get_commits(
516 def get_commits(
517 self, start_id=None, end_id=None, start_date=None, end_date=None,
517 self, start_id=None, end_id=None, start_date=None, end_date=None,
518 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
518 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
519 """
519 """
520 Returns iterator of `BaseCommit` objects from start to end
520 Returns iterator of `BaseCommit` objects from start to end
521 not inclusive. This should behave just like a list, ie. end is not
521 not inclusive. This should behave just like a list, ie. end is not
522 inclusive.
522 inclusive.
523
523
524 :param start_id: None or str, must be a valid commit id
524 :param start_id: None or str, must be a valid commit id
525 :param end_id: None or str, must be a valid commit id
525 :param end_id: None or str, must be a valid commit id
526 :param start_date:
526 :param start_date:
527 :param end_date:
527 :param end_date:
528 :param branch_name:
528 :param branch_name:
529 :param show_hidden:
529 :param show_hidden:
530 :param pre_load:
530 :param pre_load:
531 :param translate_tags:
531 :param translate_tags:
532 """
532 """
533 raise NotImplementedError
533 raise NotImplementedError
534
534
535 def __getitem__(self, key):
535 def __getitem__(self, key):
536 """
536 """
537 Allows index based access to the commit objects of this repository.
537 Allows index based access to the commit objects of this repository.
538 """
538 """
539 pre_load = ["author", "branch", "date", "message", "parents"]
539 pre_load = ["author", "branch", "date", "message", "parents"]
540 if isinstance(key, slice):
540 if isinstance(key, slice):
541 return self._get_range(key, pre_load)
541 return self._get_range(key, pre_load)
542 return self.get_commit(commit_idx=key, pre_load=pre_load)
542 return self.get_commit(commit_idx=key, pre_load=pre_load)
543
543
544 def _get_range(self, slice_obj, pre_load):
544 def _get_range(self, slice_obj, pre_load):
545 for commit_id in self.commit_ids.__getitem__(slice_obj):
545 for commit_id in self.commit_ids.__getitem__(slice_obj):
546 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
546 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
547
547
548 def count(self):
548 def count(self):
549 return len(self.commit_ids)
549 return len(self.commit_ids)
550
550
551 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
551 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
552 """
552 """
553 Creates and returns a tag for the given ``commit_id``.
553 Creates and returns a tag for the given ``commit_id``.
554
554
555 :param name: name for new tag
555 :param name: name for new tag
556 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
556 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
557 :param commit_id: commit id for which new tag would be created
557 :param commit_id: commit id for which new tag would be created
558 :param message: message of the tag's commit
558 :param message: message of the tag's commit
559 :param date: date of tag's commit
559 :param date: date of tag's commit
560
560
561 :raises TagAlreadyExistError: if tag with same name already exists
561 :raises TagAlreadyExistError: if tag with same name already exists
562 """
562 """
563 raise NotImplementedError
563 raise NotImplementedError
564
564
565 def remove_tag(self, name, user, message=None, date=None):
565 def remove_tag(self, name, user, message=None, date=None):
566 """
566 """
567 Removes tag with the given ``name``.
567 Removes tag with the given ``name``.
568
568
569 :param name: name of the tag to be removed
569 :param name: name of the tag to be removed
570 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
570 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
571 :param message: message of the tag's removal commit
571 :param message: message of the tag's removal commit
572 :param date: date of tag's removal commit
572 :param date: date of tag's removal commit
573
573
574 :raises TagDoesNotExistError: if tag with given name does not exists
574 :raises TagDoesNotExistError: if tag with given name does not exists
575 """
575 """
576 raise NotImplementedError
576 raise NotImplementedError
577
577
578 def get_diff(
578 def get_diff(
579 self, commit1, commit2, path=None, ignore_whitespace=False,
579 self, commit1, commit2, path=None, ignore_whitespace=False,
580 context=3, path1=None):
580 context=3, path1=None):
581 """
581 """
582 Returns (git like) *diff*, as plain text. Shows changes introduced by
582 Returns (git like) *diff*, as plain text. Shows changes introduced by
583 `commit2` since `commit1`.
583 `commit2` since `commit1`.
584
584
585 :param commit1: Entry point from which diff is shown. Can be
585 :param commit1: Entry point from which diff is shown. Can be
586 ``self.EMPTY_COMMIT`` - in this case, patch showing all
586 ``self.EMPTY_COMMIT`` - in this case, patch showing all
587 the changes since empty state of the repository until `commit2`
587 the changes since empty state of the repository until `commit2`
588 :param commit2: Until which commit changes should be shown.
588 :param commit2: Until which commit changes should be shown.
589 :param path: Can be set to a path of a file to create a diff of that
589 :param path: Can be set to a path of a file to create a diff of that
590 file. If `path1` is also set, this value is only associated to
590 file. If `path1` is also set, this value is only associated to
591 `commit2`.
591 `commit2`.
592 :param ignore_whitespace: If set to ``True``, would not show whitespace
592 :param ignore_whitespace: If set to ``True``, would not show whitespace
593 changes. Defaults to ``False``.
593 changes. Defaults to ``False``.
594 :param context: How many lines before/after changed lines should be
594 :param context: How many lines before/after changed lines should be
595 shown. Defaults to ``3``.
595 shown. Defaults to ``3``.
596 :param path1: Can be set to a path to associate with `commit1`. This
596 :param path1: Can be set to a path to associate with `commit1`. This
597 parameter works only for backends which support diff generation for
597 parameter works only for backends which support diff generation for
598 different paths. Other backends will raise a `ValueError` if `path1`
598 different paths. Other backends will raise a `ValueError` if `path1`
599 is set and has a different value than `path`.
599 is set and has a different value than `path`.
600 :param file_path: filter this diff by given path pattern
600 :param file_path: filter this diff by given path pattern
601 """
601 """
602 raise NotImplementedError
602 raise NotImplementedError
603
603
604 def strip(self, commit_id, branch=None):
604 def strip(self, commit_id, branch=None):
605 """
605 """
606 Strip given commit_id from the repository
606 Strip given commit_id from the repository
607 """
607 """
608 raise NotImplementedError
608 raise NotImplementedError
609
609
610 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
610 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
611 """
611 """
612 Return a latest common ancestor commit if one exists for this repo
612 Return a latest common ancestor commit if one exists for this repo
613 `commit_id1` vs `commit_id2` from `repo2`.
613 `commit_id1` vs `commit_id2` from `repo2`.
614
614
615 :param commit_id1: Commit it from this repository to use as a
615 :param commit_id1: Commit it from this repository to use as a
616 target for the comparison.
616 target for the comparison.
617 :param commit_id2: Source commit id to use for comparison.
617 :param commit_id2: Source commit id to use for comparison.
618 :param repo2: Source repository to use for comparison.
618 :param repo2: Source repository to use for comparison.
619 """
619 """
620 raise NotImplementedError
620 raise NotImplementedError
621
621
622 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
622 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
623 """
623 """
624 Compare this repository's revision `commit_id1` with `commit_id2`.
624 Compare this repository's revision `commit_id1` with `commit_id2`.
625
625
626 Returns a tuple(commits, ancestor) that would be merged from
626 Returns a tuple(commits, ancestor) that would be merged from
627 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
627 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
628 will be returned as ancestor.
628 will be returned as ancestor.
629
629
630 :param commit_id1: Commit it from this repository to use as a
630 :param commit_id1: Commit it from this repository to use as a
631 target for the comparison.
631 target for the comparison.
632 :param commit_id2: Source commit id to use for comparison.
632 :param commit_id2: Source commit id to use for comparison.
633 :param repo2: Source repository to use for comparison.
633 :param repo2: Source repository to use for comparison.
634 :param merge: If set to ``True`` will do a merge compare which also
634 :param merge: If set to ``True`` will do a merge compare which also
635 returns the common ancestor.
635 returns the common ancestor.
636 :param pre_load: Optional. List of commit attributes to load.
636 :param pre_load: Optional. List of commit attributes to load.
637 """
637 """
638 raise NotImplementedError
638 raise NotImplementedError
639
639
640 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
640 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
641 user_name='', user_email='', message='', dry_run=False,
641 user_name='', user_email='', message='', dry_run=False,
642 use_rebase=False, close_branch=False):
642 use_rebase=False, close_branch=False):
643 """
643 """
644 Merge the revisions specified in `source_ref` from `source_repo`
644 Merge the revisions specified in `source_ref` from `source_repo`
645 onto the `target_ref` of this repository.
645 onto the `target_ref` of this repository.
646
646
647 `source_ref` and `target_ref` are named tupls with the following
647 `source_ref` and `target_ref` are named tupls with the following
648 fields `type`, `name` and `commit_id`.
648 fields `type`, `name` and `commit_id`.
649
649
650 Returns a MergeResponse named tuple with the following fields
650 Returns a MergeResponse named tuple with the following fields
651 'possible', 'executed', 'source_commit', 'target_commit',
651 'possible', 'executed', 'source_commit', 'target_commit',
652 'merge_commit'.
652 'merge_commit'.
653
653
654 :param repo_id: `repo_id` target repo id.
654 :param repo_id: `repo_id` target repo id.
655 :param workspace_id: `workspace_id` unique identifier.
655 :param workspace_id: `workspace_id` unique identifier.
656 :param target_ref: `target_ref` points to the commit on top of which
656 :param target_ref: `target_ref` points to the commit on top of which
657 the `source_ref` should be merged.
657 the `source_ref` should be merged.
658 :param source_repo: The repository that contains the commits to be
658 :param source_repo: The repository that contains the commits to be
659 merged.
659 merged.
660 :param source_ref: `source_ref` points to the topmost commit from
660 :param source_ref: `source_ref` points to the topmost commit from
661 the `source_repo` which should be merged.
661 the `source_repo` which should be merged.
662 :param user_name: Merge commit `user_name`.
662 :param user_name: Merge commit `user_name`.
663 :param user_email: Merge commit `user_email`.
663 :param user_email: Merge commit `user_email`.
664 :param message: Merge commit `message`.
664 :param message: Merge commit `message`.
665 :param dry_run: If `True` the merge will not take place.
665 :param dry_run: If `True` the merge will not take place.
666 :param use_rebase: If `True` commits from the source will be rebased
666 :param use_rebase: If `True` commits from the source will be rebased
667 on top of the target instead of being merged.
667 on top of the target instead of being merged.
668 :param close_branch: If `True` branch will be close before merging it
668 :param close_branch: If `True` branch will be close before merging it
669 """
669 """
670 if dry_run:
670 if dry_run:
671 message = message or settings.MERGE_DRY_RUN_MESSAGE
671 message = message or settings.MERGE_DRY_RUN_MESSAGE
672 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
672 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
673 user_name = user_name or settings.MERGE_DRY_RUN_USER
673 user_name = user_name or settings.MERGE_DRY_RUN_USER
674 else:
674 else:
675 if not user_name:
675 if not user_name:
676 raise ValueError('user_name cannot be empty')
676 raise ValueError('user_name cannot be empty')
677 if not user_email:
677 if not user_email:
678 raise ValueError('user_email cannot be empty')
678 raise ValueError('user_email cannot be empty')
679 if not message:
679 if not message:
680 raise ValueError('message cannot be empty')
680 raise ValueError('message cannot be empty')
681
681
682 try:
682 try:
683 return self._merge_repo(
683 return self._merge_repo(
684 repo_id, workspace_id, target_ref, source_repo,
684 repo_id, workspace_id, target_ref, source_repo,
685 source_ref, message, user_name, user_email, dry_run=dry_run,
685 source_ref, message, user_name, user_email, dry_run=dry_run,
686 use_rebase=use_rebase, close_branch=close_branch)
686 use_rebase=use_rebase, close_branch=close_branch)
687 except RepositoryError as exc:
687 except RepositoryError as exc:
688 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
688 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
689 return MergeResponse(
689 return MergeResponse(
690 False, False, None, MergeFailureReason.UNKNOWN,
690 False, False, None, MergeFailureReason.UNKNOWN,
691 metadata={'exception': str(exc)})
691 metadata={'exception': str(exc)})
692
692
693 def _merge_repo(self, repo_id, workspace_id, target_ref,
693 def _merge_repo(self, repo_id, workspace_id, target_ref,
694 source_repo, source_ref, merge_message,
694 source_repo, source_ref, merge_message,
695 merger_name, merger_email, dry_run=False,
695 merger_name, merger_email, dry_run=False,
696 use_rebase=False, close_branch=False):
696 use_rebase=False, close_branch=False):
697 """Internal implementation of merge."""
697 """Internal implementation of merge."""
698 raise NotImplementedError
698 raise NotImplementedError
699
699
700 def _maybe_prepare_merge_workspace(
700 def _maybe_prepare_merge_workspace(
701 self, repo_id, workspace_id, target_ref, source_ref):
701 self, repo_id, workspace_id, target_ref, source_ref):
702 """
702 """
703 Create the merge workspace.
703 Create the merge workspace.
704
704
705 :param workspace_id: `workspace_id` unique identifier.
705 :param workspace_id: `workspace_id` unique identifier.
706 """
706 """
707 raise NotImplementedError
707 raise NotImplementedError
708
708
709 @classmethod
709 @classmethod
710 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
710 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
711 """
711 """
712 Legacy version that was used before. We still need it for
712 Legacy version that was used before. We still need it for
713 backward compat
713 backward compat
714 """
714 """
715 return os.path.join(
715 return os.path.join(
716 os.path.dirname(repo_path),
716 os.path.dirname(repo_path),
717 f'.__shadow_{os.path.basename(repo_path)}_{workspace_id}')
717 f'.__shadow_{os.path.basename(repo_path)}_{workspace_id}')
718
718
719 @classmethod
719 @classmethod
720 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
720 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
721 # The name of the shadow repository must start with '.', so it is
721 # The name of the shadow repository must start with '.', so it is
722 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
722 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
723 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
723 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
724 if os.path.exists(legacy_repository_path):
724 if os.path.exists(legacy_repository_path):
725 return legacy_repository_path
725 return legacy_repository_path
726 else:
726 else:
727 return os.path.join(
727 return os.path.join(
728 os.path.dirname(repo_path),
728 os.path.dirname(repo_path),
729 f'.__shadow_repo_{repo_id}_{workspace_id}')
729 f'.__shadow_repo_{repo_id}_{workspace_id}')
730
730
731 def cleanup_merge_workspace(self, repo_id, workspace_id):
731 def cleanup_merge_workspace(self, repo_id, workspace_id):
732 """
732 """
733 Remove merge workspace.
733 Remove merge workspace.
734
734
735 This function MUST not fail in case there is no workspace associated to
735 This function MUST not fail in case there is no workspace associated to
736 the given `workspace_id`.
736 the given `workspace_id`.
737
737
738 :param workspace_id: `workspace_id` unique identifier.
738 :param workspace_id: `workspace_id` unique identifier.
739 """
739 """
740 shadow_repository_path = self._get_shadow_repository_path(
740 shadow_repository_path = self._get_shadow_repository_path(
741 self.path, repo_id, workspace_id)
741 self.path, repo_id, workspace_id)
742 shadow_repository_path_del = '{}.{}.delete'.format(
742 shadow_repository_path_del = '{}.{}.delete'.format(
743 shadow_repository_path, time.time())
743 shadow_repository_path, time.time())
744
744
745 # move the shadow repo, so it never conflicts with the one used.
745 # move the shadow repo, so it never conflicts with the one used.
746 # we use this method because shutil.rmtree had some edge case problems
746 # we use this method because shutil.rmtree had some edge case problems
747 # removing symlinked repositories
747 # removing symlinked repositories
748 if not os.path.isdir(shadow_repository_path):
748 if not os.path.isdir(shadow_repository_path):
749 return
749 return
750
750
751 shutil.move(shadow_repository_path, shadow_repository_path_del)
751 shutil.move(shadow_repository_path, shadow_repository_path_del)
752 try:
752 try:
753 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
753 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
754 except Exception:
754 except Exception:
755 log.exception('Failed to gracefully remove shadow repo under %s',
755 log.exception('Failed to gracefully remove shadow repo under %s',
756 shadow_repository_path_del)
756 shadow_repository_path_del)
757 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
757 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
758
758
759 # ========== #
759 # ========== #
760 # COMMIT API #
760 # COMMIT API #
761 # ========== #
761 # ========== #
762
762
763 @LazyProperty
763 @LazyProperty
764 def in_memory_commit(self):
764 def in_memory_commit(self):
765 """
765 """
766 Returns :class:`InMemoryCommit` object for this repository.
766 Returns :class:`InMemoryCommit` object for this repository.
767 """
767 """
768 raise NotImplementedError
768 raise NotImplementedError
769
769
770 # ======================== #
770 # ======================== #
771 # UTILITIES FOR SUBCLASSES #
771 # UTILITIES FOR SUBCLASSES #
772 # ======================== #
772 # ======================== #
773
773
774 def _validate_diff_commits(self, commit1, commit2):
774 def _validate_diff_commits(self, commit1, commit2):
775 """
775 """
776 Validates that the given commits are related to this repository.
776 Validates that the given commits are related to this repository.
777
777
778 Intended as a utility for sub classes to have a consistent validation
778 Intended as a utility for sub classes to have a consistent validation
779 of input parameters in methods like :meth:`get_diff`.
779 of input parameters in methods like :meth:`get_diff`.
780 """
780 """
781 self._validate_commit(commit1)
781 self._validate_commit(commit1)
782 self._validate_commit(commit2)
782 self._validate_commit(commit2)
783 if (isinstance(commit1, EmptyCommit) and
783 if (isinstance(commit1, EmptyCommit) and
784 isinstance(commit2, EmptyCommit)):
784 isinstance(commit2, EmptyCommit)):
785 raise ValueError("Cannot compare two empty commits")
785 raise ValueError("Cannot compare two empty commits")
786
786
787 def _validate_commit(self, commit):
787 def _validate_commit(self, commit):
788 if not isinstance(commit, BaseCommit):
788 if not isinstance(commit, BaseCommit):
789 raise TypeError(
789 raise TypeError(
790 "%s is not of type BaseCommit" % repr(commit))
790 "%s is not of type BaseCommit" % repr(commit))
791 if commit.repository != self and not isinstance(commit, EmptyCommit):
791 if commit.repository != self and not isinstance(commit, EmptyCommit):
792 raise ValueError(
792 raise ValueError(
793 "Commit %s must be a valid commit from this repository %s, "
793 "Commit %s must be a valid commit from this repository %s, "
794 "related to this repository instead %s." %
794 "related to this repository instead %s." %
795 (commit, self, commit.repository))
795 (commit, self, commit.repository))
796
796
797 def _validate_commit_id(self, commit_id):
797 def _validate_commit_id(self, commit_id):
798 if not isinstance(commit_id, str):
798 if not isinstance(commit_id, str):
799 raise TypeError(f"commit_id must be a string value got {type(commit_id)} instead")
799 raise TypeError(f"commit_id must be a string value got {type(commit_id)} instead")
800
800
801 def _validate_commit_idx(self, commit_idx):
801 def _validate_commit_idx(self, commit_idx):
802 if not isinstance(commit_idx, int):
802 if not isinstance(commit_idx, int):
803 raise TypeError(f"commit_idx must be a numeric value, got {type(commit_idx)}")
803 raise TypeError(f"commit_idx must be a numeric value, got {type(commit_idx)}")
804
804
805 def _validate_branch_name(self, branch_name):
805 def _validate_branch_name(self, branch_name):
806 if branch_name and branch_name not in self.branches_all:
806 if branch_name and branch_name not in self.branches_all:
807 msg = (f"Branch {branch_name} not found in {self}")
807 msg = (f"Branch {branch_name} not found in {self}")
808 raise BranchDoesNotExistError(msg)
808 raise BranchDoesNotExistError(msg)
809
809
810 #
810 #
811 # Supporting deprecated API parts
811 # Supporting deprecated API parts
812 # TODO: johbo: consider to move this into a mixin
812 # TODO: johbo: consider to move this into a mixin
813 #
813 #
814
814
815 @property
815 @property
816 def EMPTY_CHANGESET(self):
816 def EMPTY_CHANGESET(self):
817 warnings.warn(
817 warnings.warn(
818 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
818 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
819 return self.EMPTY_COMMIT_ID
819 return self.EMPTY_COMMIT_ID
820
820
821 @property
821 @property
822 def revisions(self):
822 def revisions(self):
823 warnings.warn("Use commits attribute instead", DeprecationWarning)
823 warnings.warn("Use commits attribute instead", DeprecationWarning)
824 return self.commit_ids
824 return self.commit_ids
825
825
826 @revisions.setter
826 @revisions.setter
827 def revisions(self, value):
827 def revisions(self, value):
828 warnings.warn("Use commits attribute instead", DeprecationWarning)
828 warnings.warn("Use commits attribute instead", DeprecationWarning)
829 self.commit_ids = value
829 self.commit_ids = value
830
830
831 def get_changeset(self, revision=None, pre_load=None):
831 def get_changeset(self, revision=None, pre_load=None):
832 warnings.warn("Use get_commit instead", DeprecationWarning)
832 warnings.warn("Use get_commit instead", DeprecationWarning)
833 commit_id = None
833 commit_id = None
834 commit_idx = None
834 commit_idx = None
835 if isinstance(revision, str):
835 if isinstance(revision, str):
836 commit_id = revision
836 commit_id = revision
837 else:
837 else:
838 commit_idx = revision
838 commit_idx = revision
839 return self.get_commit(
839 return self.get_commit(
840 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
840 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
841
841
842 def get_changesets(
842 def get_changesets(
843 self, start=None, end=None, start_date=None, end_date=None,
843 self, start=None, end=None, start_date=None, end_date=None,
844 branch_name=None, pre_load=None):
844 branch_name=None, pre_load=None):
845 warnings.warn("Use get_commits instead", DeprecationWarning)
845 warnings.warn("Use get_commits instead", DeprecationWarning)
846 start_id = self._revision_to_commit(start)
846 start_id = self._revision_to_commit(start)
847 end_id = self._revision_to_commit(end)
847 end_id = self._revision_to_commit(end)
848 return self.get_commits(
848 return self.get_commits(
849 start_id=start_id, end_id=end_id, start_date=start_date,
849 start_id=start_id, end_id=end_id, start_date=start_date,
850 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
850 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
851
851
852 def _revision_to_commit(self, revision):
852 def _revision_to_commit(self, revision):
853 """
853 """
854 Translates a revision to a commit_id
854 Translates a revision to a commit_id
855
855
856 Helps to support the old changeset based API which allows to use
856 Helps to support the old changeset based API which allows to use
857 commit ids and commit indices interchangeable.
857 commit ids and commit indices interchangeable.
858 """
858 """
859 if revision is None:
859 if revision is None:
860 return revision
860 return revision
861
861
862 if isinstance(revision, str):
862 if isinstance(revision, str):
863 commit_id = revision
863 commit_id = revision
864 else:
864 else:
865 commit_id = self.commit_ids[revision]
865 commit_id = self.commit_ids[revision]
866 return commit_id
866 return commit_id
867
867
868 @property
868 @property
869 def in_memory_changeset(self):
869 def in_memory_changeset(self):
870 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
870 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
871 return self.in_memory_commit
871 return self.in_memory_commit
872
872
873 def get_path_permissions(self, username):
873 def get_path_permissions(self, username):
874 """
874 """
875 Returns a path permission checker or None if not supported
875 Returns a path permission checker or None if not supported
876
876
877 :param username: session user name
877 :param username: session user name
878 :return: an instance of BasePathPermissionChecker or None
878 :return: an instance of BasePathPermissionChecker or None
879 """
879 """
880 return None
880 return None
881
881
882 def install_hooks(self, force=False):
882 def install_hooks(self, force=False):
883 return self._remote.install_hooks(force)
883 return self._remote.install_hooks(force)
884
884
885 def get_hooks_info(self):
885 def get_hooks_info(self):
886 return self._remote.get_hooks_info()
886 return self._remote.get_hooks_info()
887
887
888 def vcsserver_invalidate_cache(self, delete=False):
888 def vcsserver_invalidate_cache(self, delete=False):
889 return self._remote.vcsserver_invalidate_cache(delete)
889 return self._remote.vcsserver_invalidate_cache(delete)
890
890
891
891
892 class BaseCommit(object):
892 class BaseCommit(object):
893 """
893 """
894 Each backend should implement it's commit representation.
894 Each backend should implement it's commit representation.
895
895
896 **Attributes**
896 **Attributes**
897
897
898 ``repository``
898 ``repository``
899 repository object within which commit exists
899 repository object within which commit exists
900
900
901 ``id``
901 ``id``
902 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
902 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
903 just ``tip``.
903 just ``tip``.
904
904
905 ``raw_id``
905 ``raw_id``
906 raw commit representation (i.e. full 40 length sha for git
906 raw commit representation (i.e. full 40 length sha for git
907 backend)
907 backend)
908
908
909 ``short_id``
909 ``short_id``
910 shortened (if apply) version of ``raw_id``; it would be simple
910 shortened (if apply) version of ``raw_id``; it would be simple
911 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
911 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
912 as ``raw_id`` for subversion
912 as ``raw_id`` for subversion
913
913
914 ``idx``
914 ``idx``
915 commit index
915 commit index
916
916
917 ``files``
917 ``files``
918 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
918 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
919
919
920 ``dirs``
920 ``dirs``
921 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
921 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
922
922
923 ``nodes``
923 ``nodes``
924 combined list of ``Node`` objects
924 combined list of ``Node`` objects
925
925
926 ``author``
926 ``author``
927 author of the commit, as unicode
927 author of the commit, as unicode
928
928
929 ``message``
929 ``message``
930 message of the commit, as unicode
930 message of the commit, as unicode
931
931
932 ``parents``
932 ``parents``
933 list of parent commits
933 list of parent commits
934
934
935 """
935 """
936 repository = None
936 repository = None
937 branch = None
937 branch = None
938
938
939 """
939 """
940 Depending on the backend this should be set to the branch name of the
940 Depending on the backend this should be set to the branch name of the
941 commit. Backends not supporting branches on commits should leave this
941 commit. Backends not supporting branches on commits should leave this
942 value as ``None``.
942 value as ``None``.
943 """
943 """
944
944
945 _ARCHIVE_PREFIX_TEMPLATE = '{repo_name}-{short_id}'
945 _ARCHIVE_PREFIX_TEMPLATE = '{repo_name}-{short_id}'
946 """
946 """
947 This template is used to generate a default prefix for repository archives
947 This template is used to generate a default prefix for repository archives
948 if no prefix has been specified.
948 if no prefix has been specified.
949 """
949 """
950
950
951 def __repr__(self):
951 def __repr__(self):
952 return self.__str__()
952 return self.__str__()
953
953
954 def __str__(self):
954 def __str__(self):
955 return f'<{self.__class__.__name__} at {self.idx}:{self.short_id}>'
955 return f'<{self.__class__.__name__} at {self.idx}:{self.short_id}>'
956
956
957 def __eq__(self, other):
957 def __eq__(self, other):
958 same_instance = isinstance(other, self.__class__)
958 same_instance = isinstance(other, self.__class__)
959 return same_instance and self.raw_id == other.raw_id
959 return same_instance and self.raw_id == other.raw_id
960
960
961 def __json__(self):
961 def __json__(self):
962 parents = []
962 parents = []
963 try:
963 try:
964 for parent in self.parents:
964 for parent in self.parents:
965 parents.append({'raw_id': parent.raw_id})
965 parents.append({'raw_id': parent.raw_id})
966 except NotImplementedError:
966 except NotImplementedError:
967 # empty commit doesn't have parents implemented
967 # empty commit doesn't have parents implemented
968 pass
968 pass
969
969
970 return {
970 return {
971 'short_id': self.short_id,
971 'short_id': self.short_id,
972 'raw_id': self.raw_id,
972 'raw_id': self.raw_id,
973 'revision': self.idx,
973 'revision': self.idx,
974 'message': self.message,
974 'message': self.message,
975 'date': self.date,
975 'date': self.date,
976 'author': self.author,
976 'author': self.author,
977 'parents': parents,
977 'parents': parents,
978 'branch': self.branch
978 'branch': self.branch
979 }
979 }
980
980
981 def __getstate__(self):
981 def __getstate__(self):
982 d = self.__dict__.copy()
982 d = self.__dict__.copy()
983 d.pop('_remote', None)
983 d.pop('_remote', None)
984 d.pop('repository', None)
984 d.pop('repository', None)
985 return d
985 return d
986
986
987 def get_remote(self):
987 def get_remote(self):
988 return self._remote
988 return self._remote
989
989
990 def serialize(self):
990 def serialize(self):
991 return self.__json__()
991 return self.__json__()
992
992
993 def _get_refs(self):
993 def _get_refs(self):
994 return {
994 return {
995 'branches': [self.branch] if self.branch else [],
995 'branches': [self.branch] if self.branch else [],
996 'bookmarks': getattr(self, 'bookmarks', []),
996 'bookmarks': getattr(self, 'bookmarks', []),
997 'tags': self.tags
997 'tags': self.tags
998 }
998 }
999
999
1000 @LazyProperty
1000 @LazyProperty
1001 def last(self):
1001 def last(self):
1002 """
1002 """
1003 ``True`` if this is last commit in repository, ``False``
1003 ``True`` if this is last commit in repository, ``False``
1004 otherwise; trying to access this attribute while there is no
1004 otherwise; trying to access this attribute while there is no
1005 commits would raise `EmptyRepositoryError`
1005 commits would raise `EmptyRepositoryError`
1006 """
1006 """
1007 if self.repository is None:
1007 if self.repository is None:
1008 raise CommitError("Cannot check if it's most recent commit")
1008 raise CommitError("Cannot check if it's most recent commit")
1009 return self.raw_id == self.repository.commit_ids[-1]
1009 return self.raw_id == self.repository.commit_ids[-1]
1010
1010
1011 @LazyProperty
1011 @LazyProperty
1012 def parents(self):
1012 def parents(self):
1013 """
1013 """
1014 Returns list of parent commits.
1014 Returns list of parent commits.
1015 """
1015 """
1016 raise NotImplementedError
1016 raise NotImplementedError
1017
1017
1018 @LazyProperty
1018 @LazyProperty
1019 def first_parent(self):
1019 def first_parent(self):
1020 """
1020 """
1021 Returns list of parent commits.
1021 Returns list of parent commits.
1022 """
1022 """
1023 return self.parents[0] if self.parents else EmptyCommit()
1023 return self.parents[0] if self.parents else EmptyCommit()
1024
1024
1025 @property
1025 @property
1026 def merge(self):
1026 def merge(self):
1027 """
1027 """
1028 Returns boolean if commit is a merge.
1028 Returns boolean if commit is a merge.
1029 """
1029 """
1030 return len(self.parents) > 1
1030 return len(self.parents) > 1
1031
1031
1032 @LazyProperty
1032 @LazyProperty
1033 def children(self):
1033 def children(self):
1034 """
1034 """
1035 Returns list of child commits.
1035 Returns list of child commits.
1036 """
1036 """
1037 raise NotImplementedError
1037 raise NotImplementedError
1038
1038
1039 @LazyProperty
1039 @LazyProperty
1040 def id(self):
1040 def id(self):
1041 """
1041 """
1042 Returns string identifying this commit.
1042 Returns string identifying this commit.
1043 """
1043 """
1044 raise NotImplementedError
1044 raise NotImplementedError
1045
1045
1046 @LazyProperty
1046 @LazyProperty
1047 def raw_id(self):
1047 def raw_id(self):
1048 """
1048 """
1049 Returns raw string identifying this commit.
1049 Returns raw string identifying this commit.
1050 """
1050 """
1051 raise NotImplementedError
1051 raise NotImplementedError
1052
1052
1053 @LazyProperty
1053 @LazyProperty
1054 def short_id(self):
1054 def short_id(self):
1055 """
1055 """
1056 Returns shortened version of ``raw_id`` attribute, as string,
1056 Returns shortened version of ``raw_id`` attribute, as string,
1057 identifying this commit, useful for presentation to users.
1057 identifying this commit, useful for presentation to users.
1058 """
1058 """
1059 raise NotImplementedError
1059 raise NotImplementedError
1060
1060
1061 @LazyProperty
1061 @LazyProperty
1062 def idx(self):
1062 def idx(self):
1063 """
1063 """
1064 Returns integer identifying this commit.
1064 Returns integer identifying this commit.
1065 """
1065 """
1066 raise NotImplementedError
1066 raise NotImplementedError
1067
1067
1068 @LazyProperty
1068 @LazyProperty
1069 def committer(self):
1069 def committer(self):
1070 """
1070 """
1071 Returns committer for this commit
1071 Returns committer for this commit
1072 """
1072 """
1073 raise NotImplementedError
1073 raise NotImplementedError
1074
1074
1075 @LazyProperty
1075 @LazyProperty
1076 def committer_name(self):
1076 def committer_name(self):
1077 """
1077 """
1078 Returns committer name for this commit
1078 Returns committer name for this commit
1079 """
1079 """
1080
1080
1081 return author_name(self.committer)
1081 return author_name(self.committer)
1082
1082
1083 @LazyProperty
1083 @LazyProperty
1084 def committer_email(self):
1084 def committer_email(self):
1085 """
1085 """
1086 Returns committer email address for this commit
1086 Returns committer email address for this commit
1087 """
1087 """
1088
1088
1089 return author_email(self.committer)
1089 return author_email(self.committer)
1090
1090
1091 @LazyProperty
1091 @LazyProperty
1092 def author(self):
1092 def author(self):
1093 """
1093 """
1094 Returns author for this commit
1094 Returns author for this commit
1095 """
1095 """
1096
1096
1097 raise NotImplementedError
1097 raise NotImplementedError
1098
1098
1099 @LazyProperty
1099 @LazyProperty
1100 def author_name(self):
1100 def author_name(self):
1101 """
1101 """
1102 Returns author name for this commit
1102 Returns author name for this commit
1103 """
1103 """
1104
1104
1105 return author_name(self.author)
1105 return author_name(self.author)
1106
1106
1107 @LazyProperty
1107 @LazyProperty
1108 def author_email(self):
1108 def author_email(self):
1109 """
1109 """
1110 Returns author email address for this commit
1110 Returns author email address for this commit
1111 """
1111 """
1112
1112
1113 return author_email(self.author)
1113 return author_email(self.author)
1114
1114
1115 def get_file_mode(self, path: bytes):
1115 def get_file_mode(self, path: bytes):
1116 """
1116 """
1117 Returns stat mode of the file at `path`.
1117 Returns stat mode of the file at `path`.
1118 """
1118 """
1119 raise NotImplementedError
1119 raise NotImplementedError
1120
1120
1121 def is_link(self, path):
1121 def is_link(self, path):
1122 """
1122 """
1123 Returns ``True`` if given `path` is a symlink
1123 Returns ``True`` if given `path` is a symlink
1124 """
1124 """
1125 raise NotImplementedError
1125 raise NotImplementedError
1126
1126
1127 def is_node_binary(self, path):
1127 def is_node_binary(self, path):
1128 """
1128 """
1129 Returns ``True`` is given path is a binary file
1129 Returns ``True`` is given path is a binary file
1130 """
1130 """
1131 raise NotImplementedError
1131 raise NotImplementedError
1132
1132
1133 def node_md5_hash(self, path):
1133 def node_md5_hash(self, path):
1134 """
1134 """
1135 Returns md5 hash of a node data
1135 Returns md5 hash of a node data
1136 """
1136 """
1137 raise NotImplementedError
1137 raise NotImplementedError
1138
1138
1139 def get_file_content(self, path) -> bytes:
1139 def get_file_content(self, path) -> bytes:
1140 """
1140 """
1141 Returns content of the file at the given `path`.
1141 Returns content of the file at the given `path`.
1142 """
1142 """
1143 raise NotImplementedError
1143 raise NotImplementedError
1144
1144
1145 def get_file_content_streamed(self, path):
1145 def get_file_content_streamed(self, path):
1146 """
1146 """
1147 returns a streaming response from vcsserver with file content
1147 returns a streaming response from vcsserver with file content
1148 """
1148 """
1149 raise NotImplementedError
1149 raise NotImplementedError
1150
1150
1151 def get_file_size(self, path):
1151 def get_file_size(self, path):
1152 """
1152 """
1153 Returns size of the file at the given `path`.
1153 Returns size of the file at the given `path`.
1154 """
1154 """
1155 raise NotImplementedError
1155 raise NotImplementedError
1156
1156
1157 def get_path_commit(self, path, pre_load=None):
1157 def get_path_commit(self, path, pre_load=None):
1158 """
1158 """
1159 Returns last commit of the file at the given `path`.
1159 Returns last commit of the file at the given `path`.
1160
1160
1161 :param pre_load: Optional. List of commit attributes to load.
1161 :param pre_load: Optional. List of commit attributes to load.
1162 """
1162 """
1163 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1163 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1164 if not commits:
1164 if not commits:
1165 raise RepositoryError(
1165 raise RepositoryError(
1166 'Failed to fetch history for path {}. '
1166 'Failed to fetch history for path {}. '
1167 'Please check if such path exists in your repository'.format(
1167 'Please check if such path exists in your repository'.format(
1168 path))
1168 path))
1169 return commits[0]
1169 return commits[0]
1170
1170
1171 def get_path_history(self, path, limit=None, pre_load=None):
1171 def get_path_history(self, path, limit=None, pre_load=None):
1172 """
1172 """
1173 Returns history of file as reversed list of :class:`BaseCommit`
1173 Returns history of file as reversed list of :class:`BaseCommit`
1174 objects for which file at given `path` has been modified.
1174 objects for which file at given `path` has been modified.
1175
1175
1176 :param limit: Optional. Allows to limit the size of the returned
1176 :param limit: Optional. Allows to limit the size of the returned
1177 history. This is intended as a hint to the underlying backend, so
1177 history. This is intended as a hint to the underlying backend, so
1178 that it can apply optimizations depending on the limit.
1178 that it can apply optimizations depending on the limit.
1179 :param pre_load: Optional. List of commit attributes to load.
1179 :param pre_load: Optional. List of commit attributes to load.
1180 """
1180 """
1181 raise NotImplementedError
1181 raise NotImplementedError
1182
1182
1183 def get_file_annotate(self, path, pre_load=None):
1183 def get_file_annotate(self, path, pre_load=None):
1184 """
1184 """
1185 Returns a generator of four element tuples with
1185 Returns a generator of four element tuples with
1186 lineno, sha, commit lazy loader and line
1186 lineno, sha, commit lazy loader and line
1187
1187
1188 :param pre_load: Optional. List of commit attributes to load.
1188 :param pre_load: Optional. List of commit attributes to load.
1189 """
1189 """
1190 raise NotImplementedError
1190 raise NotImplementedError
1191
1191
1192 def get_nodes(self, path, pre_load=None):
1192 def get_nodes(self, path, pre_load=None):
1193 """
1193 """
1194 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1194 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1195 state of commit at the given ``path``.
1195 state of commit at the given ``path``.
1196
1196
1197 :raises ``CommitError``: if node at the given ``path`` is not
1197 :raises ``CommitError``: if node at the given ``path`` is not
1198 instance of ``DirNode``
1198 instance of ``DirNode``
1199 """
1199 """
1200 raise NotImplementedError
1200 raise NotImplementedError
1201
1201
1202 def get_node(self, path):
1202 def get_node(self, path):
1203 """
1203 """
1204 Returns ``Node`` object from the given ``path``.
1204 Returns ``Node`` object from the given ``path``.
1205
1205
1206 :raises ``NodeDoesNotExistError``: if there is no node at the given
1206 :raises ``NodeDoesNotExistError``: if there is no node at the given
1207 ``path``
1207 ``path``
1208 """
1208 """
1209 raise NotImplementedError
1209 raise NotImplementedError
1210
1210
1211 def get_largefile_node(self, path):
1211 def get_largefile_node(self, path):
1212 """
1212 """
1213 Returns the path to largefile from Mercurial/Git-lfs storage.
1213 Returns the path to largefile from Mercurial/Git-lfs storage.
1214 or None if it's not a largefile node
1214 or None if it's not a largefile node
1215 """
1215 """
1216 return None
1216 return None
1217
1217
1218 def archive_repo(self, archive_name_key, kind='tgz', subrepos=None,
1218 def archive_repo(self, archive_name_key, kind='tgz', subrepos=None,
1219 archive_dir_name=None, write_metadata=False, mtime=None,
1219 archive_dir_name=None, write_metadata=False, mtime=None,
1220 archive_at_path='/', cache_config=None):
1220 archive_at_path='/', cache_config=None):
1221 """
1221 """
1222 Creates an archive containing the contents of the repository.
1222 Creates an archive containing the contents of the repository.
1223
1223
1224 :param archive_name_key: unique key under this archive should be generated
1224 :param archive_name_key: unique key under this archive should be generated
1225 :param kind: one of the following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1225 :param kind: one of the following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1226 :param archive_dir_name: name of root directory in archive.
1226 :param archive_dir_name: name of root directory in archive.
1227 Default is repository name and commit's short_id joined with dash:
1227 Default is repository name and commit's short_id joined with dash:
1228 ``"{repo_name}-{short_id}"``.
1228 ``"{repo_name}-{short_id}"``.
1229 :param write_metadata: write a metadata file into archive.
1229 :param write_metadata: write a metadata file into archive.
1230 :param mtime: custom modification time for archive creation, defaults
1230 :param mtime: custom modification time for archive creation, defaults
1231 to time.time() if not given.
1231 to time.time() if not given.
1232 :param archive_at_path: pack files at this path (default '/')
1232 :param archive_at_path: pack files at this path (default '/')
1233 :param cache_config: config spec to send to vcsserver to configure the backend to store files
1233 :param cache_config: config spec to send to vcsserver to configure the backend to store files
1234
1234
1235 :raise VCSError: If prefix has a problem.
1235 :raise VCSError: If prefix has a problem.
1236 """
1236 """
1237 cache_config = cache_config or {}
1237 cache_config = cache_config or {}
1238 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1238 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1239 if kind not in allowed_kinds:
1239 if kind not in allowed_kinds:
1240 raise ImproperArchiveTypeError(
1240 raise ImproperArchiveTypeError(
1241 f'Archive kind ({kind}) not supported use one of {allowed_kinds}')
1241 f'Archive kind ({kind}) not supported use one of {allowed_kinds}')
1242
1242
1243 archive_dir_name = self._validate_archive_prefix(archive_dir_name)
1243 archive_dir_name = self._validate_archive_prefix(archive_dir_name)
1244 mtime = mtime is not None or time.mktime(self.date.timetuple())
1244 mtime = mtime is not None or time.mktime(self.date.timetuple())
1245 commit_id = self.raw_id
1245 commit_id = self.raw_id
1246
1246
1247 return self.repository._remote.archive_repo(
1247 return self.repository._remote.archive_repo(
1248 archive_name_key, kind, mtime, archive_at_path,
1248 archive_name_key, kind, mtime, archive_at_path,
1249 archive_dir_name, commit_id, cache_config)
1249 archive_dir_name, commit_id, cache_config)
1250
1250
1251 def _validate_archive_prefix(self, archive_dir_name):
1251 def _validate_archive_prefix(self, archive_dir_name):
1252 if archive_dir_name is None:
1252 if archive_dir_name is None:
1253 archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format(
1253 archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format(
1254 repo_name=safe_str(self.repository.name),
1254 repo_name=safe_str(self.repository.name),
1255 short_id=self.short_id)
1255 short_id=self.short_id)
1256 elif not isinstance(archive_dir_name, str):
1256 elif not isinstance(archive_dir_name, str):
1257 raise ValueError(f"archive_dir_name is not str object but: {type(archive_dir_name)}")
1257 raise ValueError(f"archive_dir_name is not str object but: {type(archive_dir_name)}")
1258 elif archive_dir_name.startswith('/'):
1258 elif archive_dir_name.startswith('/'):
1259 raise VCSError("Prefix cannot start with leading slash")
1259 raise VCSError("Prefix cannot start with leading slash")
1260 elif archive_dir_name.strip() == '':
1260 elif archive_dir_name.strip() == '':
1261 raise VCSError("Prefix cannot be empty")
1261 raise VCSError("Prefix cannot be empty")
1262 elif not archive_dir_name.isascii():
1262 elif not archive_dir_name.isascii():
1263 raise VCSError("Prefix cannot contain non ascii characters")
1263 raise VCSError("Prefix cannot contain non ascii characters")
1264 return archive_dir_name
1264 return archive_dir_name
1265
1265
1266 @LazyProperty
1266 @LazyProperty
1267 def root(self):
1267 def root(self):
1268 """
1268 """
1269 Returns ``RootNode`` object for this commit.
1269 Returns ``RootNode`` object for this commit.
1270 """
1270 """
1271 return self.get_node('')
1271 return self.get_node('')
1272
1272
1273 def next(self, branch=None):
1273 def next(self, branch=None):
1274 """
1274 """
1275 Returns next commit from current, if branch is gives it will return
1275 Returns next commit from current, if branch is gives it will return
1276 next commit belonging to this branch
1276 next commit belonging to this branch
1277
1277
1278 :param branch: show commits within the given named branch
1278 :param branch: show commits within the given named branch
1279 """
1279 """
1280 indexes = range(self.idx + 1, self.repository.count())
1280 indexes = range(self.idx + 1, self.repository.count())
1281 return self._find_next(indexes, branch)
1281 return self._find_next(indexes, branch)
1282
1282
1283 def prev(self, branch=None):
1283 def prev(self, branch=None):
1284 """
1284 """
1285 Returns previous commit from current, if branch is gives it will
1285 Returns previous commit from current, if branch is gives it will
1286 return previous commit belonging to this branch
1286 return previous commit belonging to this branch
1287
1287
1288 :param branch: show commit within the given named branch
1288 :param branch: show commit within the given named branch
1289 """
1289 """
1290 indexes = range(self.idx - 1, -1, -1)
1290 indexes = range(self.idx - 1, -1, -1)
1291 return self._find_next(indexes, branch)
1291 return self._find_next(indexes, branch)
1292
1292
1293 def _find_next(self, indexes, branch=None):
1293 def _find_next(self, indexes, branch=None):
1294 if branch and self.branch != branch:
1294 if branch and self.branch != branch:
1295 raise VCSError('Branch option used on commit not belonging '
1295 raise VCSError('Branch option used on commit not belonging '
1296 'to that branch')
1296 'to that branch')
1297
1297
1298 for next_idx in indexes:
1298 for next_idx in indexes:
1299 commit = self.repository.get_commit(commit_idx=next_idx)
1299 commit = self.repository.get_commit(commit_idx=next_idx)
1300 if branch and branch != commit.branch:
1300 if branch and branch != commit.branch:
1301 continue
1301 continue
1302 return commit
1302 return commit
1303 raise CommitDoesNotExistError
1303 raise CommitDoesNotExistError
1304
1304
1305 def diff(self, ignore_whitespace=True, context=3):
1305 def diff(self, ignore_whitespace=True, context=3):
1306 """
1306 """
1307 Returns a `Diff` object representing the change made by this commit.
1307 Returns a `Diff` object representing the change made by this commit.
1308 """
1308 """
1309 parent = self.first_parent
1309 parent = self.first_parent
1310 diff = self.repository.get_diff(
1310 diff = self.repository.get_diff(
1311 parent, self,
1311 parent, self,
1312 ignore_whitespace=ignore_whitespace,
1312 ignore_whitespace=ignore_whitespace,
1313 context=context)
1313 context=context)
1314 return diff
1314 return diff
1315
1315
1316 @LazyProperty
1316 @LazyProperty
1317 def added(self):
1317 def added(self):
1318 """
1318 """
1319 Returns list of added ``FileNode`` objects.
1319 Returns list of added ``FileNode`` objects.
1320 """
1320 """
1321 raise NotImplementedError
1321 raise NotImplementedError
1322
1322
1323 @LazyProperty
1323 @LazyProperty
1324 def changed(self):
1324 def changed(self):
1325 """
1325 """
1326 Returns list of modified ``FileNode`` objects.
1326 Returns list of modified ``FileNode`` objects.
1327 """
1327 """
1328 raise NotImplementedError
1328 raise NotImplementedError
1329
1329
1330 @LazyProperty
1330 @LazyProperty
1331 def removed(self):
1331 def removed(self):
1332 """
1332 """
1333 Returns list of removed ``FileNode`` objects.
1333 Returns list of removed ``FileNode`` objects.
1334 """
1334 """
1335 raise NotImplementedError
1335 raise NotImplementedError
1336
1336
1337 @LazyProperty
1337 @LazyProperty
1338 def size(self):
1338 def size(self):
1339 """
1339 """
1340 Returns total number of bytes from contents of all filenodes.
1340 Returns total number of bytes from contents of all filenodes.
1341 """
1341 """
1342 return sum(node.size for node in self.get_filenodes_generator())
1342 return sum(node.size for node in self.get_filenodes_generator())
1343
1343
1344 def walk(self, topurl=''):
1344 def walk(self, topurl=''):
1345 """
1345 """
1346 Similar to os.walk method. Insted of filesystem it walks through
1346 Similar to os.walk method. Insted of filesystem it walks through
1347 commit starting at given ``topurl``. Returns generator of tuples
1347 commit starting at given ``topurl``. Returns generator of tuples
1348 (top_node, dirnodes, filenodes).
1348 (top_node, dirnodes, filenodes).
1349 """
1349 """
1350 from rhodecode.lib.vcs.nodes import DirNode
1350 from rhodecode.lib.vcs.nodes import DirNode
1351
1351
1352 if isinstance(topurl, DirNode):
1352 if isinstance(topurl, DirNode):
1353 top_node = topurl
1353 top_node = topurl
1354 else:
1354 else:
1355 top_node = self.get_node(topurl)
1355 top_node = self.get_node(topurl)
1356
1356
1357 has_default_pre_load = False
1357 has_default_pre_load = False
1358 if isinstance(top_node, DirNode):
1358 if isinstance(top_node, DirNode):
1359 # used to inject as we walk same defaults as given top_node
1359 # used to inject as we walk same defaults as given top_node
1360 default_pre_load = top_node.default_pre_load
1360 default_pre_load = top_node.default_pre_load
1361 has_default_pre_load = True
1361 has_default_pre_load = True
1362
1362
1363 if not top_node.is_dir():
1363 if not top_node.is_dir():
1364 return
1364 return
1365 yield top_node, top_node.dirs, top_node.files
1365 yield top_node, top_node.dirs, top_node.files
1366 for dir_node in top_node.dirs:
1366 for dir_node in top_node.dirs:
1367 if has_default_pre_load:
1367 if has_default_pre_load:
1368 dir_node.default_pre_load = default_pre_load
1368 dir_node.default_pre_load = default_pre_load
1369 yield from self.walk(dir_node)
1369 yield from self.walk(dir_node)
1370
1370
1371 def get_filenodes_generator(self):
1371 def get_filenodes_generator(self):
1372 """
1372 """
1373 Returns generator that yields *all* file nodes.
1373 Returns generator that yields *all* file nodes.
1374 """
1374 """
1375 for topnode, dirs, files in self.walk():
1375 for topnode, dirs, files in self.walk():
1376 yield from files
1376 yield from files
1377
1377
1378 #
1378 #
1379 # Utilities for sub classes to support consistent behavior
1379 # Utilities for sub classes to support consistent behavior
1380 #
1380 #
1381
1381
1382 def no_node_at_path(self, path):
1382 def no_node_at_path(self, path):
1383 return NodeDoesNotExistError(
1383 return NodeDoesNotExistError(
1384 f"There is no file nor directory at the given path: "
1384 f"There is no file nor directory at the given path: "
1385 f"`{safe_str(path)}` at commit {self.short_id}")
1385 f"`{safe_str(path)}` at commit {self.short_id}")
1386
1386
1387 def _fix_path(self, path: str) -> str:
1387 def _fix_path(self, path: str) -> str:
1388 """
1388 """
1389 Paths are stored without trailing slash so we need to get rid off it if
1389 Paths are stored without trailing slash so we need to get rid off it if
1390 needed.
1390 needed.
1391 """
1391 """
1392 return safe_str(path).rstrip('/')
1392 return safe_str(path).rstrip('/')
1393
1393
1394 #
1394 #
1395 # Deprecated API based on changesets
1395 # Deprecated API based on changesets
1396 #
1396 #
1397
1397
1398 @property
1398 @property
1399 def revision(self):
1399 def revision(self):
1400 warnings.warn("Use idx instead", DeprecationWarning)
1400 warnings.warn("Use idx instead", DeprecationWarning)
1401 return self.idx
1401 return self.idx
1402
1402
1403 @revision.setter
1403 @revision.setter
1404 def revision(self, value):
1404 def revision(self, value):
1405 warnings.warn("Use idx instead", DeprecationWarning)
1405 warnings.warn("Use idx instead", DeprecationWarning)
1406 self.idx = value
1406 self.idx = value
1407
1407
1408 def get_file_changeset(self, path):
1408 def get_file_changeset(self, path):
1409 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1409 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1410 return self.get_path_commit(path)
1410 return self.get_path_commit(path)
1411
1411
1412
1412
1413 class BaseChangesetClass(type):
1413 class BaseChangesetClass(type):
1414
1414
1415 def __instancecheck__(self, instance):
1415 def __instancecheck__(self, instance):
1416 return isinstance(instance, BaseCommit)
1416 return isinstance(instance, BaseCommit)
1417
1417
1418
1418
1419 class BaseChangeset(BaseCommit, metaclass=BaseChangesetClass):
1419 class BaseChangeset(BaseCommit, metaclass=BaseChangesetClass):
1420
1420
1421 def __new__(cls, *args, **kwargs):
1421 def __new__(cls, *args, **kwargs):
1422 warnings.warn(
1422 warnings.warn(
1423 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1423 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1424 return super().__new__(cls, *args, **kwargs)
1424 return super().__new__(cls, *args, **kwargs)
1425
1425
1426
1426
1427 class BaseInMemoryCommit(object):
1427 class BaseInMemoryCommit(object):
1428 """
1428 """
1429 Represents differences between repository's state (most recent head) and
1429 Represents differences between repository's state (most recent head) and
1430 changes made *in place*.
1430 changes made *in place*.
1431
1431
1432 **Attributes**
1432 **Attributes**
1433
1433
1434 ``repository``
1434 ``repository``
1435 repository object for this in-memory-commit
1435 repository object for this in-memory-commit
1436
1436
1437 ``added``
1437 ``added``
1438 list of ``FileNode`` objects marked as *added*
1438 list of ``FileNode`` objects marked as *added*
1439
1439
1440 ``changed``
1440 ``changed``
1441 list of ``FileNode`` objects marked as *changed*
1441 list of ``FileNode`` objects marked as *changed*
1442
1442
1443 ``removed``
1443 ``removed``
1444 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1444 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1445 *removed*
1445 *removed*
1446
1446
1447 ``parents``
1447 ``parents``
1448 list of :class:`BaseCommit` instances representing parents of
1448 list of :class:`BaseCommit` instances representing parents of
1449 in-memory commit. Should always be 2-element sequence.
1449 in-memory commit. Should always be 2-element sequence.
1450
1450
1451 """
1451 """
1452
1452
1453 def __init__(self, repository):
1453 def __init__(self, repository):
1454 self.repository = repository
1454 self.repository = repository
1455 self.added = []
1455 self.added = []
1456 self.changed = []
1456 self.changed = []
1457 self.removed = []
1457 self.removed = []
1458 self.parents = []
1458 self.parents = []
1459
1459
1460 def add(self, *filenodes):
1460 def add(self, *filenodes):
1461 """
1461 """
1462 Marks given ``FileNode`` objects as *to be committed*.
1462 Marks given ``FileNode`` objects as *to be committed*.
1463
1463
1464 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1464 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1465 latest commit
1465 latest commit
1466 :raises ``NodeAlreadyAddedError``: if node with same path is already
1466 :raises ``NodeAlreadyAddedError``: if node with same path is already
1467 marked as *added*
1467 marked as *added*
1468 """
1468 """
1469 # Check if not already marked as *added* first
1469 # Check if not already marked as *added* first
1470 for node in filenodes:
1470 for node in filenodes:
1471 if node.path in (n.path for n in self.added):
1471 if node.path in (n.path for n in self.added):
1472 raise NodeAlreadyAddedError(
1472 raise NodeAlreadyAddedError(
1473 "Such FileNode %s is already marked for addition"
1473 "Such FileNode %s is already marked for addition"
1474 % node.path)
1474 % node.path)
1475 for node in filenodes:
1475 for node in filenodes:
1476 self.added.append(node)
1476 self.added.append(node)
1477
1477
1478 def change(self, *filenodes):
1478 def change(self, *filenodes):
1479 """
1479 """
1480 Marks given ``FileNode`` objects to be *changed* in next commit.
1480 Marks given ``FileNode`` objects to be *changed* in next commit.
1481
1481
1482 :raises ``EmptyRepositoryError``: if there are no commits yet
1482 :raises ``EmptyRepositoryError``: if there are no commits yet
1483 :raises ``NodeAlreadyExistsError``: if node with same path is already
1483 :raises ``NodeAlreadyExistsError``: if node with same path is already
1484 marked to be *changed*
1484 marked to be *changed*
1485 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1485 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1486 marked to be *removed*
1486 marked to be *removed*
1487 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1487 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1488 commit
1488 commit
1489 :raises ``NodeNotChangedError``: if node hasn't really be changed
1489 :raises ``NodeNotChangedError``: if node hasn't really be changed
1490 """
1490 """
1491 for node in filenodes:
1491 for node in filenodes:
1492 if node.path in (n.path for n in self.removed):
1492 if node.path in (n.path for n in self.removed):
1493 raise NodeAlreadyRemovedError(
1493 raise NodeAlreadyRemovedError(
1494 "Node at %s is already marked as removed" % node.path)
1494 "Node at %s is already marked as removed" % node.path)
1495 try:
1495 try:
1496 self.repository.get_commit()
1496 self.repository.get_commit()
1497 except EmptyRepositoryError:
1497 except EmptyRepositoryError:
1498 raise EmptyRepositoryError(
1498 raise EmptyRepositoryError(
1499 "Nothing to change - try to *add* new nodes rather than "
1499 "Nothing to change - try to *add* new nodes rather than "
1500 "changing them")
1500 "changing them")
1501 for node in filenodes:
1501 for node in filenodes:
1502 if node.path in (n.path for n in self.changed):
1502 if node.path in (n.path for n in self.changed):
1503 raise NodeAlreadyChangedError(
1503 raise NodeAlreadyChangedError(
1504 "Node at '%s' is already marked as changed" % node.path)
1504 "Node at '%s' is already marked as changed" % node.path)
1505 self.changed.append(node)
1505 self.changed.append(node)
1506
1506
1507 def remove(self, *filenodes):
1507 def remove(self, *filenodes):
1508 """
1508 """
1509 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1509 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1510 *removed* in next commit.
1510 *removed* in next commit.
1511
1511
1512 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1512 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1513 be *removed*
1513 be *removed*
1514 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1514 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1515 be *changed*
1515 be *changed*
1516 """
1516 """
1517 for node in filenodes:
1517 for node in filenodes:
1518 if node.path in (n.path for n in self.removed):
1518 if node.path in (n.path for n in self.removed):
1519 raise NodeAlreadyRemovedError(
1519 raise NodeAlreadyRemovedError(
1520 "Node is already marked to for removal at %s" % node.path)
1520 "Node is already marked to for removal at %s" % node.path)
1521 if node.path in (n.path for n in self.changed):
1521 if node.path in (n.path for n in self.changed):
1522 raise NodeAlreadyChangedError(
1522 raise NodeAlreadyChangedError(
1523 "Node is already marked to be changed at %s" % node.path)
1523 "Node is already marked to be changed at %s" % node.path)
1524 # We only mark node as *removed* - real removal is done by
1524 # We only mark node as *removed* - real removal is done by
1525 # commit method
1525 # commit method
1526 self.removed.append(node)
1526 self.removed.append(node)
1527
1527
1528 def reset(self):
1528 def reset(self):
1529 """
1529 """
1530 Resets this instance to initial state (cleans ``added``, ``changed``
1530 Resets this instance to initial state (cleans ``added``, ``changed``
1531 and ``removed`` lists).
1531 and ``removed`` lists).
1532 """
1532 """
1533 self.added = []
1533 self.added = []
1534 self.changed = []
1534 self.changed = []
1535 self.removed = []
1535 self.removed = []
1536 self.parents = []
1536 self.parents = []
1537
1537
1538 def get_ipaths(self):
1538 def get_ipaths(self):
1539 """
1539 """
1540 Returns generator of paths from nodes marked as added, changed or
1540 Returns generator of paths from nodes marked as added, changed or
1541 removed.
1541 removed.
1542 """
1542 """
1543 for node in itertools.chain(self.added, self.changed, self.removed):
1543 for node in itertools.chain(self.added, self.changed, self.removed):
1544 yield node.path
1544 yield node.path
1545
1545
1546 def get_paths(self):
1546 def get_paths(self):
1547 """
1547 """
1548 Returns list of paths from nodes marked as added, changed or removed.
1548 Returns list of paths from nodes marked as added, changed or removed.
1549 """
1549 """
1550 return list(self.get_ipaths())
1550 return list(self.get_ipaths())
1551
1551
1552 def check_integrity(self, parents=None):
1552 def check_integrity(self, parents=None):
1553 """
1553 """
1554 Checks in-memory commit's integrity. Also, sets parents if not
1554 Checks in-memory commit's integrity. Also, sets parents if not
1555 already set.
1555 already set.
1556
1556
1557 :raises CommitError: if any error occurs (i.e.
1557 :raises CommitError: if any error occurs (i.e.
1558 ``NodeDoesNotExistError``).
1558 ``NodeDoesNotExistError``).
1559 """
1559 """
1560 if not self.parents:
1560 if not self.parents:
1561 parents = parents or []
1561 parents = parents or []
1562 if len(parents) == 0:
1562 if len(parents) == 0:
1563 try:
1563 try:
1564 parents = [self.repository.get_commit(), None]
1564 parents = [self.repository.get_commit(), None]
1565 except EmptyRepositoryError:
1565 except EmptyRepositoryError:
1566 parents = [None, None]
1566 parents = [None, None]
1567 elif len(parents) == 1:
1567 elif len(parents) == 1:
1568 parents += [None]
1568 parents += [None]
1569 self.parents = parents
1569 self.parents = parents
1570
1570
1571 # Local parents, only if not None
1571 # Local parents, only if not None
1572 parents = [p for p in self.parents if p]
1572 parents = [p for p in self.parents if p]
1573
1573
1574 # Check nodes marked as added
1574 # Check nodes marked as added
1575 for p in parents:
1575 for p in parents:
1576 for node in self.added:
1576 for node in self.added:
1577 try:
1577 try:
1578 p.get_node(node.path)
1578 p.get_node(node.path)
1579 except NodeDoesNotExistError:
1579 except NodeDoesNotExistError:
1580 pass
1580 pass
1581 else:
1581 else:
1582 raise NodeAlreadyExistsError(
1582 raise NodeAlreadyExistsError(
1583 f"Node `{node.path}` already exists at {p}")
1583 f"Node `{node.path}` already exists at {p}")
1584
1584
1585 # Check nodes marked as changed
1585 # Check nodes marked as changed
1586 missing = set(self.changed)
1586 missing = set(self.changed)
1587 not_changed = set(self.changed)
1587 not_changed = set(self.changed)
1588 if self.changed and not parents:
1588 if self.changed and not parents:
1589 raise NodeDoesNotExistError(str(self.changed[0].path))
1589 raise NodeDoesNotExistError(str(self.changed[0].path))
1590 for p in parents:
1590 for p in parents:
1591 for node in self.changed:
1591 for node in self.changed:
1592 try:
1592 try:
1593 old = p.get_node(node.path)
1593 old = p.get_node(node.path)
1594 missing.remove(node)
1594 missing.remove(node)
1595 # if content actually changed, remove node from not_changed
1595 # if content actually changed, remove node from not_changed
1596 if old.content != node.content:
1596 if old.content != node.content:
1597 not_changed.remove(node)
1597 not_changed.remove(node)
1598 except NodeDoesNotExistError:
1598 except NodeDoesNotExistError:
1599 pass
1599 pass
1600 if self.changed and missing:
1600 if self.changed and missing:
1601 raise NodeDoesNotExistError(
1601 raise NodeDoesNotExistError(
1602 f"Node `{node.path}` marked as modified but missing in parents: {parents}")
1602 f"Node `{node.path}` marked as modified but missing in parents: {parents}")
1603
1603
1604 if self.changed and not_changed:
1604 if self.changed and not_changed:
1605 raise NodeNotChangedError(
1605 raise NodeNotChangedError(
1606 "Node `%s` wasn't actually changed (parents: %s)"
1606 "Node `%s` wasn't actually changed (parents: %s)"
1607 % (not_changed.pop().path, parents))
1607 % (not_changed.pop().path, parents))
1608
1608
1609 # Check nodes marked as removed
1609 # Check nodes marked as removed
1610 if self.removed and not parents:
1610 if self.removed and not parents:
1611 raise NodeDoesNotExistError(
1611 raise NodeDoesNotExistError(
1612 "Cannot remove node at %s as there "
1612 "Cannot remove node at %s as there "
1613 "were no parents specified" % self.removed[0].path)
1613 "were no parents specified" % self.removed[0].path)
1614 really_removed = set()
1614 really_removed = set()
1615 for p in parents:
1615 for p in parents:
1616 for node in self.removed:
1616 for node in self.removed:
1617 try:
1617 try:
1618 p.get_node(node.path)
1618 p.get_node(node.path)
1619 really_removed.add(node)
1619 really_removed.add(node)
1620 except CommitError:
1620 except CommitError:
1621 pass
1621 pass
1622 not_removed = set(self.removed) - really_removed
1622 not_removed = set(self.removed) - really_removed
1623 if not_removed:
1623 if not_removed:
1624 # TODO: johbo: This code branch does not seem to be covered
1624 # TODO: johbo: This code branch does not seem to be covered
1625 raise NodeDoesNotExistError(
1625 raise NodeDoesNotExistError(
1626 "Cannot remove node at %s from "
1626 "Cannot remove node at %s from "
1627 "following parents: %s" % (not_removed, parents))
1627 "following parents: %s" % (not_removed, parents))
1628
1628
1629 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1629 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1630 """
1630 """
1631 Performs in-memory commit (doesn't check workdir in any way) and
1631 Performs in-memory commit (doesn't check workdir in any way) and
1632 returns newly created :class:`BaseCommit`. Updates repository's
1632 returns newly created :class:`BaseCommit`. Updates repository's
1633 attribute `commits`.
1633 attribute `commits`.
1634
1634
1635 .. note::
1635 .. note::
1636
1636
1637 While overriding this method each backend's should call
1637 While overriding this method each backend's should call
1638 ``self.check_integrity(parents)`` in the first place.
1638 ``self.check_integrity(parents)`` in the first place.
1639
1639
1640 :param message: message of the commit
1640 :param message: message of the commit
1641 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1641 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1642 :param parents: single parent or sequence of parents from which commit
1642 :param parents: single parent or sequence of parents from which commit
1643 would be derived
1643 would be derived
1644 :param date: ``datetime.datetime`` instance. Defaults to
1644 :param date: ``datetime.datetime`` instance. Defaults to
1645 ``datetime.datetime.now()``.
1645 ``datetime.datetime.now()``.
1646 :param branch: branch name, as string. If none given, default backend's
1646 :param branch: branch name, as string. If none given, default backend's
1647 branch would be used.
1647 branch would be used.
1648
1648
1649 :raises ``CommitError``: if any error occurs while committing
1649 :raises ``CommitError``: if any error occurs while committing
1650 """
1650 """
1651 raise NotImplementedError
1651 raise NotImplementedError
1652
1652
1653
1653
1654 class BaseInMemoryChangesetClass(type):
1654 class BaseInMemoryChangesetClass(type):
1655
1655
1656 def __instancecheck__(self, instance):
1656 def __instancecheck__(self, instance):
1657 return isinstance(instance, BaseInMemoryCommit)
1657 return isinstance(instance, BaseInMemoryCommit)
1658
1658
1659
1659
1660 class BaseInMemoryChangeset(BaseInMemoryCommit, metaclass=BaseInMemoryChangesetClass):
1660 class BaseInMemoryChangeset(BaseInMemoryCommit, metaclass=BaseInMemoryChangesetClass):
1661
1661
1662 def __new__(cls, *args, **kwargs):
1662 def __new__(cls, *args, **kwargs):
1663 warnings.warn(
1663 warnings.warn(
1664 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1664 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1665 return super().__new__(cls, *args, **kwargs)
1665 return super().__new__(cls, *args, **kwargs)
1666
1666
1667
1667
1668 class EmptyCommit(BaseCommit):
1668 class EmptyCommit(BaseCommit):
1669 """
1669 """
1670 An dummy empty commit. It's possible to pass hash when creating
1670 An dummy empty commit. It's possible to pass hash when creating
1671 an EmptyCommit
1671 an EmptyCommit
1672 """
1672 """
1673
1673
1674 def __init__(
1674 def __init__(
1675 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1675 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1676 message='', author='', date=None):
1676 message='', author='', date=None):
1677 self._empty_commit_id = commit_id
1677 self._empty_commit_id = commit_id
1678 # TODO: johbo: Solve idx parameter, default value does not make
1678 # TODO: johbo: Solve idx parameter, default value does not make
1679 # too much sense
1679 # too much sense
1680 self.idx = idx
1680 self.idx = idx
1681 self.message = message
1681 self.message = message
1682 self.author = author
1682 self.author = author
1683 self.date = date or datetime.datetime.fromtimestamp(0)
1683 self.date = date or datetime.datetime.fromtimestamp(0)
1684 self.repository = repo
1684 self.repository = repo
1685 self.alias = alias
1685 self.alias = alias
1686
1686
1687 @LazyProperty
1687 @LazyProperty
1688 def raw_id(self):
1688 def raw_id(self):
1689 """
1689 """
1690 Returns raw string identifying this commit, useful for web
1690 Returns raw string identifying this commit, useful for web
1691 representation.
1691 representation.
1692 """
1692 """
1693
1693
1694 return self._empty_commit_id
1694 return self._empty_commit_id
1695
1695
1696 @LazyProperty
1696 @LazyProperty
1697 def branch(self):
1697 def branch(self):
1698 if self.alias:
1698 if self.alias:
1699 from rhodecode.lib.vcs.backends import get_backend
1699 from rhodecode.lib.vcs.backends import get_backend
1700 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1700 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1701
1701
1702 @LazyProperty
1702 @LazyProperty
1703 def short_id(self):
1703 def short_id(self):
1704 return self.raw_id[:12]
1704 return self.raw_id[:12]
1705
1705
1706 @LazyProperty
1706 @LazyProperty
1707 def id(self):
1707 def id(self):
1708 return self.raw_id
1708 return self.raw_id
1709
1709
1710 def get_path_commit(self, path, pre_load=None):
1710 def get_path_commit(self, path, pre_load=None):
1711 return self
1711 return self
1712
1712
1713 def get_file_content(self, path) -> bytes:
1713 def get_file_content(self, path) -> bytes:
1714 return b''
1714 return b''
1715
1715
1716 def get_file_content_streamed(self, path):
1716 def get_file_content_streamed(self, path):
1717 yield self.get_file_content(path)
1717 yield self.get_file_content(path)
1718
1718
1719 def get_file_size(self, path):
1719 def get_file_size(self, path):
1720 return 0
1720 return 0
1721
1721
1722
1722
1723 class EmptyChangesetClass(type):
1723 class EmptyChangesetClass(type):
1724
1724
1725 def __instancecheck__(self, instance):
1725 def __instancecheck__(self, instance):
1726 return isinstance(instance, EmptyCommit)
1726 return isinstance(instance, EmptyCommit)
1727
1727
1728
1728
1729 class EmptyChangeset(EmptyCommit, metaclass=EmptyChangesetClass):
1729 class EmptyChangeset(EmptyCommit, metaclass=EmptyChangesetClass):
1730
1730
1731 def __new__(cls, *args, **kwargs):
1731 def __new__(cls, *args, **kwargs):
1732 warnings.warn(
1732 warnings.warn(
1733 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1733 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1734 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1734 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1735
1735
1736 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1736 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1737 alias=None, revision=-1, message='', author='', date=None):
1737 alias=None, revision=-1, message='', author='', date=None):
1738 if requested_revision is not None:
1738 if requested_revision is not None:
1739 warnings.warn(
1739 warnings.warn(
1740 "Parameter requested_revision not supported anymore",
1740 "Parameter requested_revision not supported anymore",
1741 DeprecationWarning)
1741 DeprecationWarning)
1742 super().__init__(
1742 super().__init__(
1743 commit_id=cs, repo=repo, alias=alias, idx=revision,
1743 commit_id=cs, repo=repo, alias=alias, idx=revision,
1744 message=message, author=author, date=date)
1744 message=message, author=author, date=date)
1745
1745
1746 @property
1746 @property
1747 def revision(self):
1747 def revision(self):
1748 warnings.warn("Use idx instead", DeprecationWarning)
1748 warnings.warn("Use idx instead", DeprecationWarning)
1749 return self.idx
1749 return self.idx
1750
1750
1751 @revision.setter
1751 @revision.setter
1752 def revision(self, value):
1752 def revision(self, value):
1753 warnings.warn("Use idx instead", DeprecationWarning)
1753 warnings.warn("Use idx instead", DeprecationWarning)
1754 self.idx = value
1754 self.idx = value
1755
1755
1756
1756
1757 class EmptyRepository(BaseRepository):
1757 class EmptyRepository(BaseRepository):
1758 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1758 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1759 pass
1759 pass
1760
1760
1761 def get_diff(self, *args, **kwargs):
1761 def get_diff(self, *args, **kwargs):
1762 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1762 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1763 return GitDiff(b'')
1763 return GitDiff(b'')
1764
1764
1765
1765
1766 class CollectionGenerator(object):
1766 class CollectionGenerator(object):
1767
1767
1768 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1768 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1769 self.repo = repo
1769 self.repo = repo
1770 self.commit_ids = commit_ids
1770 self.commit_ids = commit_ids
1771 self.collection_size = collection_size
1771 self.collection_size = collection_size
1772 self.pre_load = pre_load
1772 self.pre_load = pre_load
1773 self.translate_tag = translate_tag
1773 self.translate_tag = translate_tag
1774
1774
1775 def __len__(self):
1775 def __len__(self):
1776 if self.collection_size is not None:
1776 if self.collection_size is not None:
1777 return self.collection_size
1777 return self.collection_size
1778 return self.commit_ids.__len__()
1778 return self.commit_ids.__len__()
1779
1779
1780 def __iter__(self):
1780 def __iter__(self):
1781 for commit_id in self.commit_ids:
1781 for commit_id in self.commit_ids:
1782 # TODO: johbo: Mercurial passes in commit indices or commit ids
1782 # TODO: johbo: Mercurial passes in commit indices or commit ids
1783 yield self._commit_factory(commit_id)
1783 yield self._commit_factory(commit_id)
1784
1784
1785 def _commit_factory(self, commit_id):
1785 def _commit_factory(self, commit_id):
1786 """
1786 """
1787 Allows backends to override the way commits are generated.
1787 Allows backends to override the way commits are generated.
1788 """
1788 """
1789 return self.repo.get_commit(
1789 return self.repo.get_commit(
1790 commit_id=commit_id, pre_load=self.pre_load,
1790 commit_id=commit_id, pre_load=self.pre_load,
1791 translate_tag=self.translate_tag)
1791 translate_tag=self.translate_tag)
1792
1792
1793 def __getitem__(self, key):
1793 def __getitem__(self, key):
1794 """Return either a single element by index, or a sliced collection."""
1794 """Return either a single element by index, or a sliced collection."""
1795
1795
1796 if isinstance(key, slice):
1796 if isinstance(key, slice):
1797 commit_ids = self.commit_ids[key.start:key.stop]
1797 commit_ids = self.commit_ids[key.start:key.stop]
1798
1798
1799 else:
1799 else:
1800 # single item
1800 # single item
1801 commit_ids = self.commit_ids[key]
1801 commit_ids = self.commit_ids[key]
1802
1802
1803 return self.__class__(
1803 return self.__class__(
1804 self.repo, commit_ids, pre_load=self.pre_load,
1804 self.repo, commit_ids, pre_load=self.pre_load,
1805 translate_tag=self.translate_tag)
1805 translate_tag=self.translate_tag)
1806
1806
1807 def __repr__(self):
1807 def __repr__(self):
1808 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1808 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1809
1809
1810
1810
1811 class Config(object):
1811 class Config(object):
1812 """
1812 """
1813 Represents the configuration for a repository.
1813 Represents the configuration for a repository.
1814
1814
1815 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1815 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1816 standard library. It implements only the needed subset.
1816 standard library. It implements only the needed subset.
1817 """
1817 """
1818
1818
1819 def __init__(self):
1819 def __init__(self):
1820 self._values = {}
1820 self._values = {}
1821
1821
1822 def copy(self):
1822 def copy(self):
1823 clone = Config()
1823 clone = Config()
1824 for section, values in self._values.items():
1824 for section, values in self._values.items():
1825 clone._values[section] = values.copy()
1825 clone._values[section] = values.copy()
1826 return clone
1826 return clone
1827
1827
1828 def __repr__(self):
1828 def __repr__(self):
1829 return '<Config({} sections) at {}>'.format(
1829 return '<Config({} sections) at {}>'.format(
1830 len(self._values), hex(id(self)))
1830 len(self._values), hex(id(self)))
1831
1831
1832 def items(self, section):
1832 def items(self, section):
1833 return self._values.get(section, {}).items()
1833 return self._values.get(section, {}).items()
1834
1834
1835 def get(self, section, option):
1835 def get(self, section, option):
1836 return self._values.get(section, {}).get(option)
1836 return self._values.get(section, {}).get(option)
1837
1837
1838 def set(self, section, option, value):
1838 def set(self, section, option, value):
1839 section_values = self._values.setdefault(section, {})
1839 section_values = self._values.setdefault(section, {})
1840 section_values[option] = value
1840 section_values[option] = value
1841
1841
1842 def clear_section(self, section):
1842 def clear_section(self, section):
1843 self._values[section] = {}
1843 self._values[section] = {}
1844
1844
1845 def drop_option(self, section, option):
1846 if section not in self._values:
1847 raise ValueError(f'Section {section} does not exist')
1848 del self._values[section][option]
1849
1845 def serialize(self):
1850 def serialize(self):
1846 """
1851 """
1847 Creates a list of three tuples (section, key, value) representing
1852 Creates a list of three tuples (section, key, value) representing
1848 this config object.
1853 this config object.
1849 """
1854 """
1850 items = []
1855 items = []
1851 for section in self._values:
1856 for section in self._values:
1852 for option, value in self._values[section].items():
1857 for option, value in self._values[section].items():
1853 items.append(
1858 items.append(
1854 (safe_str(section), safe_str(option), safe_str(value)))
1859 (safe_str(section), safe_str(option), safe_str(value)))
1855 return items
1860 return items
1856
1861
1857
1862
1858 class Diff(object):
1863 class Diff(object):
1859 """
1864 """
1860 Represents a diff result from a repository backend.
1865 Represents a diff result from a repository backend.
1861
1866
1862 Subclasses have to provide a backend specific value for
1867 Subclasses have to provide a backend specific value for
1863 :attr:`_header_re` and :attr:`_meta_re`.
1868 :attr:`_header_re` and :attr:`_meta_re`.
1864 """
1869 """
1865 _meta_re = None
1870 _meta_re = None
1866 _header_re: bytes = re.compile(br"")
1871 _header_re: bytes = re.compile(br"")
1867
1872
1868 def __init__(self, raw_diff: bytes):
1873 def __init__(self, raw_diff: bytes):
1869 if not isinstance(raw_diff, bytes):
1874 if not isinstance(raw_diff, bytes):
1870 raise Exception(f'raw_diff must be bytes - got {type(raw_diff)}')
1875 raise Exception(f'raw_diff must be bytes - got {type(raw_diff)}')
1871
1876
1872 self.raw = memoryview(raw_diff)
1877 self.raw = memoryview(raw_diff)
1873
1878
1874 def get_header_re(self):
1879 def get_header_re(self):
1875 return self._header_re
1880 return self._header_re
1876
1881
1877 def chunks(self):
1882 def chunks(self):
1878 """
1883 """
1879 split the diff in chunks of separate --git a/file b/file chunks
1884 split the diff in chunks of separate --git a/file b/file chunks
1880 to make diffs consistent we must prepend with \n, and make sure
1885 to make diffs consistent we must prepend with \n, and make sure
1881 we can detect last chunk as this was also has special rule
1886 we can detect last chunk as this was also has special rule
1882 """
1887 """
1883
1888
1884 diff_parts = (b'\n' + bytes(self.raw)).split(b'\ndiff --git')
1889 diff_parts = (b'\n' + bytes(self.raw)).split(b'\ndiff --git')
1885
1890
1886 chunks = diff_parts[1:]
1891 chunks = diff_parts[1:]
1887 total_chunks = len(chunks)
1892 total_chunks = len(chunks)
1888
1893
1889 def diff_iter(_chunks):
1894 def diff_iter(_chunks):
1890 for cur_chunk, chunk in enumerate(_chunks, start=1):
1895 for cur_chunk, chunk in enumerate(_chunks, start=1):
1891 yield DiffChunk(chunk, self, cur_chunk == total_chunks)
1896 yield DiffChunk(chunk, self, cur_chunk == total_chunks)
1892 return diff_iter(chunks)
1897 return diff_iter(chunks)
1893
1898
1894
1899
1895 class DiffChunk(object):
1900 class DiffChunk(object):
1896
1901
1897 def __init__(self, chunk: bytes, diff_obj: Diff, is_last_chunk: bool):
1902 def __init__(self, chunk: bytes, diff_obj: Diff, is_last_chunk: bool):
1898 self.diff_obj = diff_obj
1903 self.diff_obj = diff_obj
1899
1904
1900 # since we split by \ndiff --git that part is lost from original diff
1905 # since we split by \ndiff --git that part is lost from original diff
1901 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1906 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1902 if not is_last_chunk:
1907 if not is_last_chunk:
1903 chunk += b'\n'
1908 chunk += b'\n'
1904 header_re = self.diff_obj.get_header_re()
1909 header_re = self.diff_obj.get_header_re()
1905 match = header_re.match(chunk)
1910 match = header_re.match(chunk)
1906 self.header = match.groupdict()
1911 self.header = match.groupdict()
1907 self.diff = chunk[match.end():]
1912 self.diff = chunk[match.end():]
1908 self.raw = chunk
1913 self.raw = chunk
1909
1914
1910 @property
1915 @property
1911 def header_as_str(self):
1916 def header_as_str(self):
1912 if self.header:
1917 if self.header:
1913 def safe_str_on_bytes(val):
1918 def safe_str_on_bytes(val):
1914 if isinstance(val, bytes):
1919 if isinstance(val, bytes):
1915 return safe_str(val)
1920 return safe_str(val)
1916 return val
1921 return val
1917 return {safe_str(k): safe_str_on_bytes(v) for k, v in self.header.items()}
1922 return {safe_str(k): safe_str_on_bytes(v) for k, v in self.header.items()}
1918
1923
1919 def __repr__(self):
1924 def __repr__(self):
1920 return f'DiffChunk({self.header_as_str})'
1925 return f'DiffChunk({self.header_as_str})'
1921
1926
1922
1927
1923 class BasePathPermissionChecker(object):
1928 class BasePathPermissionChecker(object):
1924
1929
1925 @staticmethod
1930 @staticmethod
1926 def create_from_patterns(includes, excludes):
1931 def create_from_patterns(includes, excludes):
1927 if includes and '*' in includes and not excludes:
1932 if includes and '*' in includes and not excludes:
1928 return AllPathPermissionChecker()
1933 return AllPathPermissionChecker()
1929 elif excludes and '*' in excludes:
1934 elif excludes and '*' in excludes:
1930 return NonePathPermissionChecker()
1935 return NonePathPermissionChecker()
1931 else:
1936 else:
1932 return PatternPathPermissionChecker(includes, excludes)
1937 return PatternPathPermissionChecker(includes, excludes)
1933
1938
1934 @property
1939 @property
1935 def has_full_access(self):
1940 def has_full_access(self):
1936 raise NotImplementedError()
1941 raise NotImplementedError()
1937
1942
1938 def has_access(self, path):
1943 def has_access(self, path):
1939 raise NotImplementedError()
1944 raise NotImplementedError()
1940
1945
1941
1946
1942 class AllPathPermissionChecker(BasePathPermissionChecker):
1947 class AllPathPermissionChecker(BasePathPermissionChecker):
1943
1948
1944 @property
1949 @property
1945 def has_full_access(self):
1950 def has_full_access(self):
1946 return True
1951 return True
1947
1952
1948 def has_access(self, path):
1953 def has_access(self, path):
1949 return True
1954 return True
1950
1955
1951
1956
1952 class NonePathPermissionChecker(BasePathPermissionChecker):
1957 class NonePathPermissionChecker(BasePathPermissionChecker):
1953
1958
1954 @property
1959 @property
1955 def has_full_access(self):
1960 def has_full_access(self):
1956 return False
1961 return False
1957
1962
1958 def has_access(self, path):
1963 def has_access(self, path):
1959 return False
1964 return False
1960
1965
1961
1966
1962 class PatternPathPermissionChecker(BasePathPermissionChecker):
1967 class PatternPathPermissionChecker(BasePathPermissionChecker):
1963
1968
1964 def __init__(self, includes, excludes):
1969 def __init__(self, includes, excludes):
1965 self.includes = includes
1970 self.includes = includes
1966 self.excludes = excludes
1971 self.excludes = excludes
1967 self.includes_re = [] if not includes else [
1972 self.includes_re = [] if not includes else [
1968 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1973 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1969 self.excludes_re = [] if not excludes else [
1974 self.excludes_re = [] if not excludes else [
1970 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1975 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1971
1976
1972 @property
1977 @property
1973 def has_full_access(self):
1978 def has_full_access(self):
1974 return '*' in self.includes and not self.excludes
1979 return '*' in self.includes and not self.excludes
1975
1980
1976 def has_access(self, path):
1981 def has_access(self, path):
1977 for regex in self.excludes_re:
1982 for regex in self.excludes_re:
1978 if regex.match(path):
1983 if regex.match(path):
1979 return False
1984 return False
1980 for regex in self.includes_re:
1985 for regex in self.includes_re:
1981 if regex.match(path):
1986 if regex.match(path):
1982 return True
1987 return True
1983 return False
1988 return False
@@ -1,1017 +1,1024 b''
1 # Copyright (C) 2014-2023 RhodeCode GmbH
1 # Copyright (C) 2014-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 HG repository module
20 HG repository module
21 """
21 """
22 import os
22 import os
23 import logging
23 import logging
24 import binascii
24 import binascii
25 import configparser
25 import configparser
26 import urllib.request
26 import urllib.request
27 import urllib.parse
27 import urllib.parse
28 import urllib.error
28 import urllib.error
29
29
30 from zope.cachedescriptors.property import Lazy as LazyProperty
30 from zope.cachedescriptors.property import Lazy as LazyProperty
31
31
32 from collections import OrderedDict
32 from collections import OrderedDict
33 from rhodecode.lib.datelib import (
33 from rhodecode.lib.datelib import (
34 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate)
34 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate)
35 from rhodecode.lib.str_utils import safe_str
35 from rhodecode.lib.str_utils import safe_str
36 from rhodecode.lib.utils2 import CachedProperty
36 from rhodecode.lib.utils2 import CachedProperty
37 from rhodecode.lib.vcs import connection, exceptions
37 from rhodecode.lib.vcs import connection, exceptions
38 from rhodecode.lib.vcs.backends.base import (
38 from rhodecode.lib.vcs.backends.base import (
39 BaseRepository, CollectionGenerator, Config, MergeResponse,
39 BaseRepository, CollectionGenerator, Config, MergeResponse,
40 MergeFailureReason, Reference, BasePathPermissionChecker)
40 MergeFailureReason, Reference, BasePathPermissionChecker)
41 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
41 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
42 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
42 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
43 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
43 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
45 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
46 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError, UnresolvedFilesInRepo)
46 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError, UnresolvedFilesInRepo)
47
47
48 hexlify = binascii.hexlify
48 hexlify = binascii.hexlify
49 nullid = "\0" * 20
49 nullid = "\0" * 20
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 class MercurialRepository(BaseRepository):
54 class MercurialRepository(BaseRepository):
55 """
55 """
56 Mercurial repository backend
56 Mercurial repository backend
57 """
57 """
58 DEFAULT_BRANCH_NAME = 'default'
58 DEFAULT_BRANCH_NAME = 'default'
59
59
60 def __init__(self, repo_path, config=None, create=False, src_url=None,
60 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 do_workspace_checkout=False, with_wire=None, bare=False):
61 do_workspace_checkout=False, with_wire=None, bare=False):
62 """
62 """
63 Raises RepositoryError if repository could not be find at the given
63 Raises RepositoryError if repository could not be find at the given
64 ``repo_path``.
64 ``repo_path``.
65
65
66 :param repo_path: local path of the repository
66 :param repo_path: local path of the repository
67 :param config: config object containing the repo configuration
67 :param config: config object containing the repo configuration
68 :param create=False: if set to True, would try to create repository if
68 :param create=False: if set to True, would try to create repository if
69 it does not exist rather than raising exception
69 it does not exist rather than raising exception
70 :param src_url=None: would try to clone repository from given location
70 :param src_url=None: would try to clone repository from given location
71 :param do_workspace_checkout=False: sets update of working copy after
71 :param do_workspace_checkout=False: sets update of working copy after
72 making a clone
72 making a clone
73 :param bare: not used, compatible with other VCS
73 :param bare: not used, compatible with other VCS
74 """
74 """
75
75
76 self.path = safe_str(os.path.abspath(repo_path))
76 self.path = safe_str(os.path.abspath(repo_path))
77 # mercurial since 4.4.X requires certain configuration to be present
77 # mercurial since 4.4.X requires certain configuration to be present
78 # because sometimes we init the repos with config we need to meet
78 # because sometimes we init the repos with config we need to meet
79 # special requirements
79 # special requirements
80 self.config = config if config else self.get_default_config(
80 self.config = config if config else self.get_default_config(
81 default=[('extensions', 'largefiles', '')])
81 default=[('extensions', 'largefiles', '')])
82
83 # NOTE(marcink): since python3 hgsubversion is deprecated.
84 # From old installations we might still have this set enabled
85 # we explicitly remove this now here to make sure it wont propagate further
86 if config.get('extensions', 'hgsubversion') is not None:
87 config.drop_option('extensions', 'hgsubversion')
88
82 self.with_wire = with_wire or {"cache": False} # default should not use cache
89 self.with_wire = with_wire or {"cache": False} # default should not use cache
83
90
84 self._init_repo(create, src_url, do_workspace_checkout)
91 self._init_repo(create, src_url, do_workspace_checkout)
85
92
86 # caches
93 # caches
87 self._commit_ids = {}
94 self._commit_ids = {}
88
95
89 @LazyProperty
96 @LazyProperty
90 def _remote(self):
97 def _remote(self):
91 repo_id = self.path
98 repo_id = self.path
92 return connection.Hg(self.path, repo_id, self.config, with_wire=self.with_wire)
99 return connection.Hg(self.path, repo_id, self.config, with_wire=self.with_wire)
93
100
94 @CachedProperty
101 @CachedProperty
95 def commit_ids(self):
102 def commit_ids(self):
96 """
103 """
97 Returns list of commit ids, in ascending order. Being lazy
104 Returns list of commit ids, in ascending order. Being lazy
98 attribute allows external tools to inject shas from cache.
105 attribute allows external tools to inject shas from cache.
99 """
106 """
100 commit_ids = self._get_all_commit_ids()
107 commit_ids = self._get_all_commit_ids()
101 self._rebuild_cache(commit_ids)
108 self._rebuild_cache(commit_ids)
102 return commit_ids
109 return commit_ids
103
110
104 def _rebuild_cache(self, commit_ids):
111 def _rebuild_cache(self, commit_ids):
105 self._commit_ids = {commit_id: index
112 self._commit_ids = {commit_id: index
106 for index, commit_id in enumerate(commit_ids)}
113 for index, commit_id in enumerate(commit_ids)}
107
114
108 @CachedProperty
115 @CachedProperty
109 def branches(self):
116 def branches(self):
110 return self._get_branches()
117 return self._get_branches()
111
118
112 @CachedProperty
119 @CachedProperty
113 def branches_closed(self):
120 def branches_closed(self):
114 return self._get_branches(active=False, closed=True)
121 return self._get_branches(active=False, closed=True)
115
122
116 @CachedProperty
123 @CachedProperty
117 def branches_all(self):
124 def branches_all(self):
118 all_branches = {}
125 all_branches = {}
119 all_branches.update(self.branches)
126 all_branches.update(self.branches)
120 all_branches.update(self.branches_closed)
127 all_branches.update(self.branches_closed)
121 return all_branches
128 return all_branches
122
129
123 def _get_branches(self, active=True, closed=False):
130 def _get_branches(self, active=True, closed=False):
124 """
131 """
125 Gets branches for this repository
132 Gets branches for this repository
126 Returns only not closed active branches by default
133 Returns only not closed active branches by default
127
134
128 :param active: return also active branches
135 :param active: return also active branches
129 :param closed: return also closed branches
136 :param closed: return also closed branches
130
137
131 """
138 """
132 if self.is_empty():
139 if self.is_empty():
133 return {}
140 return {}
134
141
135 def get_name(ctx):
142 def get_name(ctx):
136 return ctx[0]
143 return ctx[0]
137
144
138 _branches = [(n, h,) for n, h in
145 _branches = [(n, h,) for n, h in
139 self._remote.branches(active, closed).items()]
146 self._remote.branches(active, closed).items()]
140
147
141 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
148 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
142
149
143 @CachedProperty
150 @CachedProperty
144 def tags(self):
151 def tags(self):
145 """
152 """
146 Gets tags for this repository
153 Gets tags for this repository
147 """
154 """
148 return self._get_tags()
155 return self._get_tags()
149
156
150 def _get_tags(self):
157 def _get_tags(self):
151 if self.is_empty():
158 if self.is_empty():
152 return {}
159 return {}
153
160
154 def get_name(ctx):
161 def get_name(ctx):
155 return ctx[0]
162 return ctx[0]
156
163
157 _tags = [(n, h,) for n, h in
164 _tags = [(n, h,) for n, h in
158 self._remote.tags().items()]
165 self._remote.tags().items()]
159
166
160 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
167 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
161
168
162 def tag(self, name, user, commit_id=None, message=None, date=None, **kwargs):
169 def tag(self, name, user, commit_id=None, message=None, date=None, **kwargs):
163 """
170 """
164 Creates and returns a tag for the given ``commit_id``.
171 Creates and returns a tag for the given ``commit_id``.
165
172
166 :param name: name for new tag
173 :param name: name for new tag
167 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
174 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
168 :param commit_id: commit id for which new tag would be created
175 :param commit_id: commit id for which new tag would be created
169 :param message: message of the tag's commit
176 :param message: message of the tag's commit
170 :param date: date of tag's commit
177 :param date: date of tag's commit
171
178
172 :raises TagAlreadyExistError: if tag with same name already exists
179 :raises TagAlreadyExistError: if tag with same name already exists
173 """
180 """
174 if name in self.tags:
181 if name in self.tags:
175 raise TagAlreadyExistError("Tag %s already exists" % name)
182 raise TagAlreadyExistError("Tag %s already exists" % name)
176
183
177 commit = self.get_commit(commit_id=commit_id)
184 commit = self.get_commit(commit_id=commit_id)
178 local = kwargs.setdefault('local', False)
185 local = kwargs.setdefault('local', False)
179
186
180 if message is None:
187 if message is None:
181 message = f"Added tag {name} for commit {commit.short_id}"
188 message = f"Added tag {name} for commit {commit.short_id}"
182
189
183 date, tz = date_to_timestamp_plus_offset(date)
190 date, tz = date_to_timestamp_plus_offset(date)
184
191
185 self._remote.tag(name, commit.raw_id, message, local, user, date, tz)
192 self._remote.tag(name, commit.raw_id, message, local, user, date, tz)
186 self._remote.invalidate_vcs_cache()
193 self._remote.invalidate_vcs_cache()
187
194
188 # Reinitialize tags
195 # Reinitialize tags
189 self._invalidate_prop_cache('tags')
196 self._invalidate_prop_cache('tags')
190 tag_id = self.tags[name]
197 tag_id = self.tags[name]
191
198
192 return self.get_commit(commit_id=tag_id)
199 return self.get_commit(commit_id=tag_id)
193
200
194 def remove_tag(self, name, user, message=None, date=None):
201 def remove_tag(self, name, user, message=None, date=None):
195 """
202 """
196 Removes tag with the given `name`.
203 Removes tag with the given `name`.
197
204
198 :param name: name of the tag to be removed
205 :param name: name of the tag to be removed
199 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
206 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
200 :param message: message of the tag's removal commit
207 :param message: message of the tag's removal commit
201 :param date: date of tag's removal commit
208 :param date: date of tag's removal commit
202
209
203 :raises TagDoesNotExistError: if tag with given name does not exists
210 :raises TagDoesNotExistError: if tag with given name does not exists
204 """
211 """
205 if name not in self.tags:
212 if name not in self.tags:
206 raise TagDoesNotExistError("Tag %s does not exist" % name)
213 raise TagDoesNotExistError("Tag %s does not exist" % name)
207
214
208 if message is None:
215 if message is None:
209 message = "Removed tag %s" % name
216 message = "Removed tag %s" % name
210 local = False
217 local = False
211
218
212 date, tz = date_to_timestamp_plus_offset(date)
219 date, tz = date_to_timestamp_plus_offset(date)
213
220
214 self._remote.tag(name, nullid, message, local, user, date, tz)
221 self._remote.tag(name, nullid, message, local, user, date, tz)
215 self._remote.invalidate_vcs_cache()
222 self._remote.invalidate_vcs_cache()
216 self._invalidate_prop_cache('tags')
223 self._invalidate_prop_cache('tags')
217
224
218 @LazyProperty
225 @LazyProperty
219 def bookmarks(self):
226 def bookmarks(self):
220 """
227 """
221 Gets bookmarks for this repository
228 Gets bookmarks for this repository
222 """
229 """
223 return self._get_bookmarks()
230 return self._get_bookmarks()
224
231
225 def _get_bookmarks(self):
232 def _get_bookmarks(self):
226 if self.is_empty():
233 if self.is_empty():
227 return {}
234 return {}
228
235
229 def get_name(ctx):
236 def get_name(ctx):
230 return ctx[0]
237 return ctx[0]
231
238
232 _bookmarks = [
239 _bookmarks = [
233 (n, h) for n, h in
240 (n, h) for n, h in
234 self._remote.bookmarks().items()]
241 self._remote.bookmarks().items()]
235
242
236 return OrderedDict(sorted(_bookmarks, key=get_name))
243 return OrderedDict(sorted(_bookmarks, key=get_name))
237
244
238 def _get_all_commit_ids(self):
245 def _get_all_commit_ids(self):
239 return self._remote.get_all_commit_ids('visible')
246 return self._remote.get_all_commit_ids('visible')
240
247
241 def get_diff(
248 def get_diff(
242 self, commit1, commit2, path='', ignore_whitespace=False,
249 self, commit1, commit2, path='', ignore_whitespace=False,
243 context=3, path1=None):
250 context=3, path1=None):
244 """
251 """
245 Returns (git like) *diff*, as plain text. Shows changes introduced by
252 Returns (git like) *diff*, as plain text. Shows changes introduced by
246 `commit2` since `commit1`.
253 `commit2` since `commit1`.
247
254
248 :param commit1: Entry point from which diff is shown. Can be
255 :param commit1: Entry point from which diff is shown. Can be
249 ``self.EMPTY_COMMIT`` - in this case, patch showing all
256 ``self.EMPTY_COMMIT`` - in this case, patch showing all
250 the changes since empty state of the repository until `commit2`
257 the changes since empty state of the repository until `commit2`
251 :param commit2: Until which commit changes should be shown.
258 :param commit2: Until which commit changes should be shown.
252 :param ignore_whitespace: If set to ``True``, would not show whitespace
259 :param ignore_whitespace: If set to ``True``, would not show whitespace
253 changes. Defaults to ``False``.
260 changes. Defaults to ``False``.
254 :param context: How many lines before/after changed lines should be
261 :param context: How many lines before/after changed lines should be
255 shown. Defaults to ``3``.
262 shown. Defaults to ``3``.
256 """
263 """
257 self._validate_diff_commits(commit1, commit2)
264 self._validate_diff_commits(commit1, commit2)
258 if path1 is not None and path1 != path:
265 if path1 is not None and path1 != path:
259 raise ValueError("Diff of two different paths not supported.")
266 raise ValueError("Diff of two different paths not supported.")
260
267
261 if path:
268 if path:
262 file_filter = [self.path, path]
269 file_filter = [self.path, path]
263 else:
270 else:
264 file_filter = None
271 file_filter = None
265
272
266 diff = self._remote.diff(
273 diff = self._remote.diff(
267 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
274 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
268 opt_git=True, opt_ignorews=ignore_whitespace,
275 opt_git=True, opt_ignorews=ignore_whitespace,
269 context=context)
276 context=context)
270 return MercurialDiff(diff)
277 return MercurialDiff(diff)
271
278
272 def strip(self, commit_id, branch=None):
279 def strip(self, commit_id, branch=None):
273 self._remote.strip(commit_id, update=False, backup=False)
280 self._remote.strip(commit_id, update=False, backup=False)
274
281
275 self._remote.invalidate_vcs_cache()
282 self._remote.invalidate_vcs_cache()
276 # clear cache
283 # clear cache
277 self._invalidate_prop_cache('commit_ids')
284 self._invalidate_prop_cache('commit_ids')
278
285
279 return len(self.commit_ids)
286 return len(self.commit_ids)
280
287
281 def verify(self):
288 def verify(self):
282 verify = self._remote.verify()
289 verify = self._remote.verify()
283
290
284 self._remote.invalidate_vcs_cache()
291 self._remote.invalidate_vcs_cache()
285 return verify
292 return verify
286
293
287 def hg_update_cache(self):
294 def hg_update_cache(self):
288 update_cache = self._remote.hg_update_cache()
295 update_cache = self._remote.hg_update_cache()
289
296
290 self._remote.invalidate_vcs_cache()
297 self._remote.invalidate_vcs_cache()
291 return update_cache
298 return update_cache
292
299
293 def hg_rebuild_fn_cache(self):
300 def hg_rebuild_fn_cache(self):
294 update_cache = self._remote.hg_rebuild_fn_cache()
301 update_cache = self._remote.hg_rebuild_fn_cache()
295
302
296 self._remote.invalidate_vcs_cache()
303 self._remote.invalidate_vcs_cache()
297 return update_cache
304 return update_cache
298
305
299 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
306 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
300 log.debug('Calculating common ancestor between %sc1:%s and %sc2:%s',
307 log.debug('Calculating common ancestor between %sc1:%s and %sc2:%s',
301 self, commit_id1, repo2, commit_id2)
308 self, commit_id1, repo2, commit_id2)
302
309
303 if commit_id1 == commit_id2:
310 if commit_id1 == commit_id2:
304 return commit_id1
311 return commit_id1
305
312
306 ancestors = self._remote.revs_from_revspec(
313 ancestors = self._remote.revs_from_revspec(
307 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
314 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
308 other_path=repo2.path)
315 other_path=repo2.path)
309
316
310 ancestor_id = repo2[ancestors[0]].raw_id if ancestors else None
317 ancestor_id = repo2[ancestors[0]].raw_id if ancestors else None
311
318
312 log.debug('Found common ancestor with sha: %s', ancestor_id)
319 log.debug('Found common ancestor with sha: %s', ancestor_id)
313 return ancestor_id
320 return ancestor_id
314
321
315 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
322 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
316 if commit_id1 == commit_id2:
323 if commit_id1 == commit_id2:
317 commits = []
324 commits = []
318 else:
325 else:
319 if merge:
326 if merge:
320 indexes = self._remote.revs_from_revspec(
327 indexes = self._remote.revs_from_revspec(
321 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
328 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
322 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
329 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
323 else:
330 else:
324 indexes = self._remote.revs_from_revspec(
331 indexes = self._remote.revs_from_revspec(
325 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
332 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
326 commit_id1, other_path=repo2.path)
333 commit_id1, other_path=repo2.path)
327
334
328 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
335 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
329 for idx in indexes]
336 for idx in indexes]
330
337
331 return commits
338 return commits
332
339
333 @staticmethod
340 @staticmethod
334 def check_url(url, config):
341 def check_url(url, config):
335 """
342 """
336 Function will check given url and try to verify if it's a valid
343 Function will check given url and try to verify if it's a valid
337 link. Sometimes it may happened that mercurial will issue basic
344 link. Sometimes it may happened that mercurial will issue basic
338 auth request that can cause whole API to hang when used from python
345 auth request that can cause whole API to hang when used from python
339 or other external calls.
346 or other external calls.
340
347
341 On failures it'll raise urllib2.HTTPError, exception is also thrown
348 On failures it'll raise urllib2.HTTPError, exception is also thrown
342 when the return code is non 200
349 when the return code is non 200
343 """
350 """
344 # check first if it's not an local url
351 # check first if it's not an local url
345 if os.path.isdir(url) or url.startswith('file:'):
352 if os.path.isdir(url) or url.startswith('file:'):
346 return True
353 return True
347
354
348 # Request the _remote to verify the url
355 # Request the _remote to verify the url
349 return connection.Hg.check_url(url, config.serialize())
356 return connection.Hg.check_url(url, config.serialize())
350
357
351 @staticmethod
358 @staticmethod
352 def is_valid_repository(path):
359 def is_valid_repository(path):
353 return os.path.isdir(os.path.join(path, '.hg'))
360 return os.path.isdir(os.path.join(path, '.hg'))
354
361
355 def _init_repo(self, create, src_url=None, do_workspace_checkout=False):
362 def _init_repo(self, create, src_url=None, do_workspace_checkout=False):
356 """
363 """
357 Function will check for mercurial repository in given path. If there
364 Function will check for mercurial repository in given path. If there
358 is no repository in that path it will raise an exception unless
365 is no repository in that path it will raise an exception unless
359 `create` parameter is set to True - in that case repository would
366 `create` parameter is set to True - in that case repository would
360 be created.
367 be created.
361
368
362 If `src_url` is given, would try to clone repository from the
369 If `src_url` is given, would try to clone repository from the
363 location at given clone_point. Additionally it'll make update to
370 location at given clone_point. Additionally it'll make update to
364 working copy accordingly to `do_workspace_checkout` flag.
371 working copy accordingly to `do_workspace_checkout` flag.
365 """
372 """
366 if create and os.path.exists(self.path):
373 if create and os.path.exists(self.path):
367 raise RepositoryError(
374 raise RepositoryError(
368 f"Cannot create repository at {self.path}, location already exist")
375 f"Cannot create repository at {self.path}, location already exist")
369
376
370 if src_url:
377 if src_url:
371 url = str(self._get_url(src_url))
378 url = str(self._get_url(src_url))
372 MercurialRepository.check_url(url, self.config)
379 MercurialRepository.check_url(url, self.config)
373
380
374 self._remote.clone(url, self.path, do_workspace_checkout)
381 self._remote.clone(url, self.path, do_workspace_checkout)
375
382
376 # Don't try to create if we've already cloned repo
383 # Don't try to create if we've already cloned repo
377 create = False
384 create = False
378
385
379 if create:
386 if create:
380 os.makedirs(self.path, mode=0o755)
387 os.makedirs(self.path, mode=0o755)
381
388
382 self._remote.localrepository(create)
389 self._remote.localrepository(create)
383
390
384 @LazyProperty
391 @LazyProperty
385 def in_memory_commit(self):
392 def in_memory_commit(self):
386 return MercurialInMemoryCommit(self)
393 return MercurialInMemoryCommit(self)
387
394
388 @LazyProperty
395 @LazyProperty
389 def description(self):
396 def description(self):
390 description = self._remote.get_config_value(
397 description = self._remote.get_config_value(
391 'web', 'description', untrusted=True)
398 'web', 'description', untrusted=True)
392 return safe_str(description or self.DEFAULT_DESCRIPTION)
399 return safe_str(description or self.DEFAULT_DESCRIPTION)
393
400
394 @LazyProperty
401 @LazyProperty
395 def contact(self):
402 def contact(self):
396 contact = (
403 contact = (
397 self._remote.get_config_value("web", "contact") or
404 self._remote.get_config_value("web", "contact") or
398 self._remote.get_config_value("ui", "username"))
405 self._remote.get_config_value("ui", "username"))
399 return safe_str(contact or self.DEFAULT_CONTACT)
406 return safe_str(contact or self.DEFAULT_CONTACT)
400
407
401 @LazyProperty
408 @LazyProperty
402 def last_change(self):
409 def last_change(self):
403 """
410 """
404 Returns last change made on this repository as
411 Returns last change made on this repository as
405 `datetime.datetime` object.
412 `datetime.datetime` object.
406 """
413 """
407 try:
414 try:
408 return self.get_commit().date
415 return self.get_commit().date
409 except RepositoryError:
416 except RepositoryError:
410 tzoffset = makedate()[1]
417 tzoffset = makedate()[1]
411 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
418 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
412
419
413 def _get_fs_mtime(self):
420 def _get_fs_mtime(self):
414 # fallback to filesystem
421 # fallback to filesystem
415 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
422 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
416 st_path = os.path.join(self.path, '.hg', "store")
423 st_path = os.path.join(self.path, '.hg', "store")
417 if os.path.exists(cl_path):
424 if os.path.exists(cl_path):
418 return os.stat(cl_path).st_mtime
425 return os.stat(cl_path).st_mtime
419 else:
426 else:
420 return os.stat(st_path).st_mtime
427 return os.stat(st_path).st_mtime
421
428
422 def _get_url(self, url):
429 def _get_url(self, url):
423 """
430 """
424 Returns normalized url. If schema is not given, would fall
431 Returns normalized url. If schema is not given, would fall
425 to filesystem
432 to filesystem
426 (``file:///``) schema.
433 (``file:///``) schema.
427 """
434 """
428 if url != 'default' and '://' not in url:
435 if url != 'default' and '://' not in url:
429 url = "file:" + urllib.request.pathname2url(url)
436 url = "file:" + urllib.request.pathname2url(url)
430 return url
437 return url
431
438
432 def get_hook_location(self):
439 def get_hook_location(self):
433 """
440 """
434 returns absolute path to location where hooks are stored
441 returns absolute path to location where hooks are stored
435 """
442 """
436 return os.path.join(self.path, '.hg', '.hgrc')
443 return os.path.join(self.path, '.hg', '.hgrc')
437
444
438 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
445 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
439 translate_tag=None, maybe_unreachable=False, reference_obj=None):
446 translate_tag=None, maybe_unreachable=False, reference_obj=None):
440 """
447 """
441 Returns ``MercurialCommit`` object representing repository's
448 Returns ``MercurialCommit`` object representing repository's
442 commit at the given `commit_id` or `commit_idx`.
449 commit at the given `commit_id` or `commit_idx`.
443 """
450 """
444 if self.is_empty():
451 if self.is_empty():
445 raise EmptyRepositoryError("There are no commits yet")
452 raise EmptyRepositoryError("There are no commits yet")
446
453
447 if commit_id is not None:
454 if commit_id is not None:
448 self._validate_commit_id(commit_id)
455 self._validate_commit_id(commit_id)
449 try:
456 try:
450 # we have cached idx, use it without contacting the remote
457 # we have cached idx, use it without contacting the remote
451 idx = self._commit_ids[commit_id]
458 idx = self._commit_ids[commit_id]
452 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
459 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
453 except KeyError:
460 except KeyError:
454 pass
461 pass
455
462
456 elif commit_idx is not None:
463 elif commit_idx is not None:
457 self._validate_commit_idx(commit_idx)
464 self._validate_commit_idx(commit_idx)
458 try:
465 try:
459 _commit_id = self.commit_ids[commit_idx]
466 _commit_id = self.commit_ids[commit_idx]
460 if commit_idx < 0:
467 if commit_idx < 0:
461 commit_idx = self.commit_ids.index(_commit_id)
468 commit_idx = self.commit_ids.index(_commit_id)
462
469
463 return MercurialCommit(self, _commit_id, commit_idx, pre_load=pre_load)
470 return MercurialCommit(self, _commit_id, commit_idx, pre_load=pre_load)
464 except IndexError:
471 except IndexError:
465 commit_id = commit_idx
472 commit_id = commit_idx
466 else:
473 else:
467 commit_id = "tip"
474 commit_id = "tip"
468
475
469 # case here is no cached version, do an actual lookup instead
476 # case here is no cached version, do an actual lookup instead
470 try:
477 try:
471 raw_id, idx = self._remote.lookup(commit_id, both=True)
478 raw_id, idx = self._remote.lookup(commit_id, both=True)
472 except CommitDoesNotExistError:
479 except CommitDoesNotExistError:
473 msg = "Commit {} does not exist for `{}`".format(
480 msg = "Commit {} does not exist for `{}`".format(
474 *map(safe_str, [commit_id, self.name]))
481 *map(safe_str, [commit_id, self.name]))
475 raise CommitDoesNotExistError(msg)
482 raise CommitDoesNotExistError(msg)
476
483
477 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
484 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
478
485
479 def get_commits(
486 def get_commits(
480 self, start_id=None, end_id=None, start_date=None, end_date=None,
487 self, start_id=None, end_id=None, start_date=None, end_date=None,
481 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
488 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
482 """
489 """
483 Returns generator of ``MercurialCommit`` objects from start to end
490 Returns generator of ``MercurialCommit`` objects from start to end
484 (both are inclusive)
491 (both are inclusive)
485
492
486 :param start_id: None, str(commit_id)
493 :param start_id: None, str(commit_id)
487 :param end_id: None, str(commit_id)
494 :param end_id: None, str(commit_id)
488 :param start_date: if specified, commits with commit date less than
495 :param start_date: if specified, commits with commit date less than
489 ``start_date`` would be filtered out from returned set
496 ``start_date`` would be filtered out from returned set
490 :param end_date: if specified, commits with commit date greater than
497 :param end_date: if specified, commits with commit date greater than
491 ``end_date`` would be filtered out from returned set
498 ``end_date`` would be filtered out from returned set
492 :param branch_name: if specified, commits not reachable from given
499 :param branch_name: if specified, commits not reachable from given
493 branch would be filtered out from returned set
500 branch would be filtered out from returned set
494 :param show_hidden: Show hidden commits such as obsolete or hidden from
501 :param show_hidden: Show hidden commits such as obsolete or hidden from
495 Mercurial evolve
502 Mercurial evolve
496 :raise BranchDoesNotExistError: If given ``branch_name`` does not
503 :raise BranchDoesNotExistError: If given ``branch_name`` does not
497 exist.
504 exist.
498 :raise CommitDoesNotExistError: If commit for given ``start`` or
505 :raise CommitDoesNotExistError: If commit for given ``start`` or
499 ``end`` could not be found.
506 ``end`` could not be found.
500 """
507 """
501 # actually we should check now if it's not an empty repo
508 # actually we should check now if it's not an empty repo
502 if self.is_empty():
509 if self.is_empty():
503 raise EmptyRepositoryError("There are no commits yet")
510 raise EmptyRepositoryError("There are no commits yet")
504 self._validate_branch_name(branch_name)
511 self._validate_branch_name(branch_name)
505
512
506 branch_ancestors = False
513 branch_ancestors = False
507 if start_id is not None:
514 if start_id is not None:
508 self._validate_commit_id(start_id)
515 self._validate_commit_id(start_id)
509 c_start = self.get_commit(commit_id=start_id)
516 c_start = self.get_commit(commit_id=start_id)
510 start_pos = self._commit_ids[c_start.raw_id]
517 start_pos = self._commit_ids[c_start.raw_id]
511 else:
518 else:
512 start_pos = None
519 start_pos = None
513
520
514 if end_id is not None:
521 if end_id is not None:
515 self._validate_commit_id(end_id)
522 self._validate_commit_id(end_id)
516 c_end = self.get_commit(commit_id=end_id)
523 c_end = self.get_commit(commit_id=end_id)
517 end_pos = max(0, self._commit_ids[c_end.raw_id])
524 end_pos = max(0, self._commit_ids[c_end.raw_id])
518 else:
525 else:
519 end_pos = None
526 end_pos = None
520
527
521 if None not in [start_id, end_id] and start_pos > end_pos:
528 if None not in [start_id, end_id] and start_pos > end_pos:
522 raise RepositoryError(
529 raise RepositoryError(
523 "Start commit '%s' cannot be after end commit '%s'" %
530 "Start commit '%s' cannot be after end commit '%s'" %
524 (start_id, end_id))
531 (start_id, end_id))
525
532
526 if end_pos is not None:
533 if end_pos is not None:
527 end_pos += 1
534 end_pos += 1
528
535
529 commit_filter = []
536 commit_filter = []
530
537
531 if branch_name and not branch_ancestors:
538 if branch_name and not branch_ancestors:
532 commit_filter.append(f'branch("{branch_name}")')
539 commit_filter.append(f'branch("{branch_name}")')
533 elif branch_name and branch_ancestors:
540 elif branch_name and branch_ancestors:
534 commit_filter.append(f'ancestors(branch("{branch_name}"))')
541 commit_filter.append(f'ancestors(branch("{branch_name}"))')
535
542
536 if start_date and not end_date:
543 if start_date and not end_date:
537 commit_filter.append(f'date(">{start_date}")')
544 commit_filter.append(f'date(">{start_date}")')
538 if end_date and not start_date:
545 if end_date and not start_date:
539 commit_filter.append(f'date("<{end_date}")')
546 commit_filter.append(f'date("<{end_date}")')
540 if start_date and end_date:
547 if start_date and end_date:
541 commit_filter.append(
548 commit_filter.append(
542 f'date(">{start_date}") and date("<{end_date}")')
549 f'date(">{start_date}") and date("<{end_date}")')
543
550
544 if not show_hidden:
551 if not show_hidden:
545 commit_filter.append('not obsolete()')
552 commit_filter.append('not obsolete()')
546 commit_filter.append('not hidden()')
553 commit_filter.append('not hidden()')
547
554
548 # TODO: johbo: Figure out a simpler way for this solution
555 # TODO: johbo: Figure out a simpler way for this solution
549 collection_generator = CollectionGenerator
556 collection_generator = CollectionGenerator
550 if commit_filter:
557 if commit_filter:
551 commit_filter = ' and '.join(map(safe_str, commit_filter))
558 commit_filter = ' and '.join(map(safe_str, commit_filter))
552 revisions = self._remote.rev_range([commit_filter])
559 revisions = self._remote.rev_range([commit_filter])
553 collection_generator = MercurialIndexBasedCollectionGenerator
560 collection_generator = MercurialIndexBasedCollectionGenerator
554 else:
561 else:
555 revisions = self.commit_ids
562 revisions = self.commit_ids
556
563
557 if start_pos or end_pos:
564 if start_pos or end_pos:
558 revisions = revisions[start_pos:end_pos]
565 revisions = revisions[start_pos:end_pos]
559
566
560 return collection_generator(self, revisions, pre_load=pre_load)
567 return collection_generator(self, revisions, pre_load=pre_load)
561
568
562 def pull(self, url, commit_ids=None):
569 def pull(self, url, commit_ids=None):
563 """
570 """
564 Pull changes from external location.
571 Pull changes from external location.
565
572
566 :param commit_ids: Optional. Can be set to a list of commit ids
573 :param commit_ids: Optional. Can be set to a list of commit ids
567 which shall be pulled from the other repository.
574 which shall be pulled from the other repository.
568 """
575 """
569 url = self._get_url(url)
576 url = self._get_url(url)
570 self._remote.pull(url, commit_ids=commit_ids)
577 self._remote.pull(url, commit_ids=commit_ids)
571 self._remote.invalidate_vcs_cache()
578 self._remote.invalidate_vcs_cache()
572
579
573 def fetch(self, url, commit_ids=None):
580 def fetch(self, url, commit_ids=None):
574 """
581 """
575 Backward compatibility with GIT fetch==pull
582 Backward compatibility with GIT fetch==pull
576 """
583 """
577 return self.pull(url, commit_ids=commit_ids)
584 return self.pull(url, commit_ids=commit_ids)
578
585
579 def push(self, url):
586 def push(self, url):
580 url = self._get_url(url)
587 url = self._get_url(url)
581 self._remote.sync_push(url)
588 self._remote.sync_push(url)
582
589
583 def _local_clone(self, clone_path):
590 def _local_clone(self, clone_path):
584 """
591 """
585 Create a local clone of the current repo.
592 Create a local clone of the current repo.
586 """
593 """
587 self._remote.clone(self.path, clone_path, update_after_clone=True,
594 self._remote.clone(self.path, clone_path, update_after_clone=True,
588 hooks=False)
595 hooks=False)
589
596
590 def _update(self, revision, clean=False):
597 def _update(self, revision, clean=False):
591 """
598 """
592 Update the working copy to the specified revision.
599 Update the working copy to the specified revision.
593 """
600 """
594 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
601 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
595 self._remote.update(revision, clean=clean)
602 self._remote.update(revision, clean=clean)
596
603
597 def _identify(self):
604 def _identify(self):
598 """
605 """
599 Return the current state of the working directory.
606 Return the current state of the working directory.
600 """
607 """
601 return self._remote.identify().strip().rstrip('+')
608 return self._remote.identify().strip().rstrip('+')
602
609
603 def _heads(self, branch=None):
610 def _heads(self, branch=None):
604 """
611 """
605 Return the commit ids of the repository heads.
612 Return the commit ids of the repository heads.
606 """
613 """
607 return self._remote.heads(branch=branch).strip().split(' ')
614 return self._remote.heads(branch=branch).strip().split(' ')
608
615
609 def _ancestor(self, revision1, revision2):
616 def _ancestor(self, revision1, revision2):
610 """
617 """
611 Return the common ancestor of the two revisions.
618 Return the common ancestor of the two revisions.
612 """
619 """
613 return self._remote.ancestor(revision1, revision2)
620 return self._remote.ancestor(revision1, revision2)
614
621
615 def _local_push(
622 def _local_push(
616 self, revision, repository_path, push_branches=False,
623 self, revision, repository_path, push_branches=False,
617 enable_hooks=False):
624 enable_hooks=False):
618 """
625 """
619 Push the given revision to the specified repository.
626 Push the given revision to the specified repository.
620
627
621 :param push_branches: allow to create branches in the target repo.
628 :param push_branches: allow to create branches in the target repo.
622 """
629 """
623 self._remote.push(
630 self._remote.push(
624 [revision], repository_path, hooks=enable_hooks,
631 [revision], repository_path, hooks=enable_hooks,
625 push_branches=push_branches)
632 push_branches=push_branches)
626
633
627 def _local_merge(self, target_ref, merge_message, user_name, user_email,
634 def _local_merge(self, target_ref, merge_message, user_name, user_email,
628 source_ref, use_rebase=False, close_commit_id=None, dry_run=False):
635 source_ref, use_rebase=False, close_commit_id=None, dry_run=False):
629 """
636 """
630 Merge the given source_revision into the checked out revision.
637 Merge the given source_revision into the checked out revision.
631
638
632 Returns the commit id of the merge and a boolean indicating if the
639 Returns the commit id of the merge and a boolean indicating if the
633 commit needs to be pushed.
640 commit needs to be pushed.
634 """
641 """
635
642
636 source_ref_commit_id = source_ref.commit_id
643 source_ref_commit_id = source_ref.commit_id
637 target_ref_commit_id = target_ref.commit_id
644 target_ref_commit_id = target_ref.commit_id
638
645
639 # update our workdir to target ref, for proper merge
646 # update our workdir to target ref, for proper merge
640 self._update(target_ref_commit_id, clean=True)
647 self._update(target_ref_commit_id, clean=True)
641
648
642 ancestor = self._ancestor(target_ref_commit_id, source_ref_commit_id)
649 ancestor = self._ancestor(target_ref_commit_id, source_ref_commit_id)
643 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
650 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
644
651
645 if close_commit_id:
652 if close_commit_id:
646 # NOTE(marcink): if we get the close commit, this is our new source
653 # NOTE(marcink): if we get the close commit, this is our new source
647 # which will include the close commit itself.
654 # which will include the close commit itself.
648 source_ref_commit_id = close_commit_id
655 source_ref_commit_id = close_commit_id
649
656
650 if ancestor == source_ref_commit_id:
657 if ancestor == source_ref_commit_id:
651 # Nothing to do, the changes were already integrated
658 # Nothing to do, the changes were already integrated
652 return target_ref_commit_id, False
659 return target_ref_commit_id, False
653
660
654 elif ancestor == target_ref_commit_id and is_the_same_branch:
661 elif ancestor == target_ref_commit_id and is_the_same_branch:
655 # In this case we should force a commit message
662 # In this case we should force a commit message
656 return source_ref_commit_id, True
663 return source_ref_commit_id, True
657
664
658 unresolved = None
665 unresolved = None
659 if use_rebase:
666 if use_rebase:
660 try:
667 try:
661 bookmark_name = f'rcbook{source_ref_commit_id}{target_ref_commit_id}'
668 bookmark_name = f'rcbook{source_ref_commit_id}{target_ref_commit_id}'
662 self.bookmark(bookmark_name, revision=source_ref.commit_id)
669 self.bookmark(bookmark_name, revision=source_ref.commit_id)
663 self._remote.rebase(
670 self._remote.rebase(
664 source=source_ref_commit_id, dest=target_ref_commit_id)
671 source=source_ref_commit_id, dest=target_ref_commit_id)
665 self._remote.invalidate_vcs_cache()
672 self._remote.invalidate_vcs_cache()
666 self._update(bookmark_name, clean=True)
673 self._update(bookmark_name, clean=True)
667 return self._identify(), True
674 return self._identify(), True
668 except RepositoryError as e:
675 except RepositoryError as e:
669 # The rebase-abort may raise another exception which 'hides'
676 # The rebase-abort may raise another exception which 'hides'
670 # the original one, therefore we log it here.
677 # the original one, therefore we log it here.
671 log.exception('Error while rebasing shadow repo during merge.')
678 log.exception('Error while rebasing shadow repo during merge.')
672 if 'unresolved conflicts' in safe_str(e):
679 if 'unresolved conflicts' in safe_str(e):
673 unresolved = self._remote.get_unresolved_files()
680 unresolved = self._remote.get_unresolved_files()
674 log.debug('unresolved files: %s', unresolved)
681 log.debug('unresolved files: %s', unresolved)
675
682
676 # Cleanup any rebase leftovers
683 # Cleanup any rebase leftovers
677 self._remote.invalidate_vcs_cache()
684 self._remote.invalidate_vcs_cache()
678 self._remote.rebase(abort=True)
685 self._remote.rebase(abort=True)
679 self._remote.invalidate_vcs_cache()
686 self._remote.invalidate_vcs_cache()
680 self._remote.update(clean=True)
687 self._remote.update(clean=True)
681 if unresolved:
688 if unresolved:
682 raise UnresolvedFilesInRepo(unresolved)
689 raise UnresolvedFilesInRepo(unresolved)
683 else:
690 else:
684 raise
691 raise
685 else:
692 else:
686 try:
693 try:
687 self._remote.merge(source_ref_commit_id)
694 self._remote.merge(source_ref_commit_id)
688 self._remote.invalidate_vcs_cache()
695 self._remote.invalidate_vcs_cache()
689 self._remote.commit(
696 self._remote.commit(
690 message=safe_str(merge_message),
697 message=safe_str(merge_message),
691 username=safe_str(f'{user_name} <{user_email}>'))
698 username=safe_str(f'{user_name} <{user_email}>'))
692 self._remote.invalidate_vcs_cache()
699 self._remote.invalidate_vcs_cache()
693 return self._identify(), True
700 return self._identify(), True
694 except RepositoryError as e:
701 except RepositoryError as e:
695 # The merge-abort may raise another exception which 'hides'
702 # The merge-abort may raise another exception which 'hides'
696 # the original one, therefore we log it here.
703 # the original one, therefore we log it here.
697 log.exception('Error while merging shadow repo during merge.')
704 log.exception('Error while merging shadow repo during merge.')
698 if 'unresolved merge conflicts' in safe_str(e):
705 if 'unresolved merge conflicts' in safe_str(e):
699 unresolved = self._remote.get_unresolved_files()
706 unresolved = self._remote.get_unresolved_files()
700 log.debug('unresolved files: %s', unresolved)
707 log.debug('unresolved files: %s', unresolved)
701
708
702 # Cleanup any merge leftovers
709 # Cleanup any merge leftovers
703 self._remote.update(clean=True)
710 self._remote.update(clean=True)
704 if unresolved:
711 if unresolved:
705 raise UnresolvedFilesInRepo(unresolved)
712 raise UnresolvedFilesInRepo(unresolved)
706 else:
713 else:
707 raise
714 raise
708
715
709 def _local_close(self, target_ref, user_name, user_email,
716 def _local_close(self, target_ref, user_name, user_email,
710 source_ref, close_message=''):
717 source_ref, close_message=''):
711 """
718 """
712 Close the branch of the given source_revision
719 Close the branch of the given source_revision
713
720
714 Returns the commit id of the close and a boolean indicating if the
721 Returns the commit id of the close and a boolean indicating if the
715 commit needs to be pushed.
722 commit needs to be pushed.
716 """
723 """
717 self._update(source_ref.commit_id)
724 self._update(source_ref.commit_id)
718 message = close_message or f"Closing branch: `{source_ref.name}`"
725 message = close_message or f"Closing branch: `{source_ref.name}`"
719 try:
726 try:
720 self._remote.commit(
727 self._remote.commit(
721 message=safe_str(message),
728 message=safe_str(message),
722 username=safe_str(f'{user_name} <{user_email}>'),
729 username=safe_str(f'{user_name} <{user_email}>'),
723 close_branch=True)
730 close_branch=True)
724 self._remote.invalidate_vcs_cache()
731 self._remote.invalidate_vcs_cache()
725 return self._identify(), True
732 return self._identify(), True
726 except RepositoryError:
733 except RepositoryError:
727 # Cleanup any commit leftovers
734 # Cleanup any commit leftovers
728 self._remote.update(clean=True)
735 self._remote.update(clean=True)
729 raise
736 raise
730
737
731 def _is_the_same_branch(self, target_ref, source_ref):
738 def _is_the_same_branch(self, target_ref, source_ref):
732 return (
739 return (
733 self._get_branch_name(target_ref) ==
740 self._get_branch_name(target_ref) ==
734 self._get_branch_name(source_ref))
741 self._get_branch_name(source_ref))
735
742
736 def _get_branch_name(self, ref):
743 def _get_branch_name(self, ref):
737 if ref.type == 'branch':
744 if ref.type == 'branch':
738 return ref.name
745 return ref.name
739 return self._remote.ctx_branch(ref.commit_id)
746 return self._remote.ctx_branch(ref.commit_id)
740
747
741 def _maybe_prepare_merge_workspace(
748 def _maybe_prepare_merge_workspace(
742 self, repo_id, workspace_id, unused_target_ref, unused_source_ref):
749 self, repo_id, workspace_id, unused_target_ref, unused_source_ref):
743 shadow_repository_path = self._get_shadow_repository_path(
750 shadow_repository_path = self._get_shadow_repository_path(
744 self.path, repo_id, workspace_id)
751 self.path, repo_id, workspace_id)
745 if not os.path.exists(shadow_repository_path):
752 if not os.path.exists(shadow_repository_path):
746 self._local_clone(shadow_repository_path)
753 self._local_clone(shadow_repository_path)
747 log.debug(
754 log.debug(
748 'Prepared shadow repository in %s', shadow_repository_path)
755 'Prepared shadow repository in %s', shadow_repository_path)
749
756
750 return shadow_repository_path
757 return shadow_repository_path
751
758
752 def _merge_repo(self, repo_id, workspace_id, target_ref,
759 def _merge_repo(self, repo_id, workspace_id, target_ref,
753 source_repo, source_ref, merge_message,
760 source_repo, source_ref, merge_message,
754 merger_name, merger_email, dry_run=False,
761 merger_name, merger_email, dry_run=False,
755 use_rebase=False, close_branch=False):
762 use_rebase=False, close_branch=False):
756
763
757 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
764 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
758 'rebase' if use_rebase else 'merge', dry_run)
765 'rebase' if use_rebase else 'merge', dry_run)
759
766
760 if target_ref.commit_id not in self._heads():
767 if target_ref.commit_id not in self._heads():
761 return MergeResponse(
768 return MergeResponse(
762 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
769 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
763 metadata={'target_ref': target_ref})
770 metadata={'target_ref': target_ref})
764
771
765 try:
772 try:
766 if target_ref.type == 'branch' and len(self._heads(target_ref.name)) != 1:
773 if target_ref.type == 'branch' and len(self._heads(target_ref.name)) != 1:
767 heads_all = self._heads(target_ref.name)
774 heads_all = self._heads(target_ref.name)
768 max_heads = 10
775 max_heads = 10
769 if len(heads_all) > max_heads:
776 if len(heads_all) > max_heads:
770 heads = '\n,'.join(
777 heads = '\n,'.join(
771 heads_all[:max_heads] +
778 heads_all[:max_heads] +
772 [f'and {len(heads_all)-max_heads} more.'])
779 [f'and {len(heads_all)-max_heads} more.'])
773 else:
780 else:
774 heads = '\n,'.join(heads_all)
781 heads = '\n,'.join(heads_all)
775 metadata = {
782 metadata = {
776 'target_ref': target_ref,
783 'target_ref': target_ref,
777 'source_ref': source_ref,
784 'source_ref': source_ref,
778 'heads': heads
785 'heads': heads
779 }
786 }
780 return MergeResponse(
787 return MergeResponse(
781 False, False, None,
788 False, False, None,
782 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
789 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
783 metadata=metadata)
790 metadata=metadata)
784 except CommitDoesNotExistError:
791 except CommitDoesNotExistError:
785 log.exception('Failure when looking up branch heads on hg target')
792 log.exception('Failure when looking up branch heads on hg target')
786 return MergeResponse(
793 return MergeResponse(
787 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
794 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
788 metadata={'target_ref': target_ref})
795 metadata={'target_ref': target_ref})
789
796
790 shadow_repository_path = self._maybe_prepare_merge_workspace(
797 shadow_repository_path = self._maybe_prepare_merge_workspace(
791 repo_id, workspace_id, target_ref, source_ref)
798 repo_id, workspace_id, target_ref, source_ref)
792 shadow_repo = self.get_shadow_instance(shadow_repository_path)
799 shadow_repo = self.get_shadow_instance(shadow_repository_path)
793
800
794 log.debug('Pulling in target reference %s', target_ref)
801 log.debug('Pulling in target reference %s', target_ref)
795 self._validate_pull_reference(target_ref)
802 self._validate_pull_reference(target_ref)
796 shadow_repo._local_pull(self.path, target_ref)
803 shadow_repo._local_pull(self.path, target_ref)
797
804
798 try:
805 try:
799 log.debug('Pulling in source reference %s', source_ref)
806 log.debug('Pulling in source reference %s', source_ref)
800 source_repo._validate_pull_reference(source_ref)
807 source_repo._validate_pull_reference(source_ref)
801 shadow_repo._local_pull(source_repo.path, source_ref)
808 shadow_repo._local_pull(source_repo.path, source_ref)
802 except CommitDoesNotExistError:
809 except CommitDoesNotExistError:
803 log.exception('Failure when doing local pull on hg shadow repo')
810 log.exception('Failure when doing local pull on hg shadow repo')
804 return MergeResponse(
811 return MergeResponse(
805 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
812 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
806 metadata={'source_ref': source_ref})
813 metadata={'source_ref': source_ref})
807
814
808 merge_ref = None
815 merge_ref = None
809 merge_commit_id = None
816 merge_commit_id = None
810 close_commit_id = None
817 close_commit_id = None
811 merge_failure_reason = MergeFailureReason.NONE
818 merge_failure_reason = MergeFailureReason.NONE
812 metadata = {}
819 metadata = {}
813
820
814 # enforce that close branch should be used only in case we source from
821 # enforce that close branch should be used only in case we source from
815 # an actual Branch
822 # an actual Branch
816 close_branch = close_branch and source_ref.type == 'branch'
823 close_branch = close_branch and source_ref.type == 'branch'
817
824
818 # don't allow to close branch if source and target are the same
825 # don't allow to close branch if source and target are the same
819 close_branch = close_branch and source_ref.name != target_ref.name
826 close_branch = close_branch and source_ref.name != target_ref.name
820
827
821 needs_push_on_close = False
828 needs_push_on_close = False
822 if close_branch and not use_rebase and not dry_run:
829 if close_branch and not use_rebase and not dry_run:
823 try:
830 try:
824 close_commit_id, needs_push_on_close = shadow_repo._local_close(
831 close_commit_id, needs_push_on_close = shadow_repo._local_close(
825 target_ref, merger_name, merger_email, source_ref)
832 target_ref, merger_name, merger_email, source_ref)
826 merge_possible = True
833 merge_possible = True
827 except RepositoryError:
834 except RepositoryError:
828 log.exception('Failure when doing close branch on '
835 log.exception('Failure when doing close branch on '
829 'shadow repo: %s', shadow_repo)
836 'shadow repo: %s', shadow_repo)
830 merge_possible = False
837 merge_possible = False
831 merge_failure_reason = MergeFailureReason.MERGE_FAILED
838 merge_failure_reason = MergeFailureReason.MERGE_FAILED
832 else:
839 else:
833 merge_possible = True
840 merge_possible = True
834
841
835 needs_push = False
842 needs_push = False
836 if merge_possible:
843 if merge_possible:
837
844
838 try:
845 try:
839 merge_commit_id, needs_push = shadow_repo._local_merge(
846 merge_commit_id, needs_push = shadow_repo._local_merge(
840 target_ref, merge_message, merger_name, merger_email,
847 target_ref, merge_message, merger_name, merger_email,
841 source_ref, use_rebase=use_rebase,
848 source_ref, use_rebase=use_rebase,
842 close_commit_id=close_commit_id, dry_run=dry_run)
849 close_commit_id=close_commit_id, dry_run=dry_run)
843 merge_possible = True
850 merge_possible = True
844
851
845 # read the state of the close action, if it
852 # read the state of the close action, if it
846 # maybe required a push
853 # maybe required a push
847 needs_push = needs_push or needs_push_on_close
854 needs_push = needs_push or needs_push_on_close
848
855
849 # Set a bookmark pointing to the merge commit. This bookmark
856 # Set a bookmark pointing to the merge commit. This bookmark
850 # may be used to easily identify the last successful merge
857 # may be used to easily identify the last successful merge
851 # commit in the shadow repository.
858 # commit in the shadow repository.
852 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
859 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
853 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
860 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
854 except SubrepoMergeError:
861 except SubrepoMergeError:
855 log.exception(
862 log.exception(
856 'Subrepo merge error during local merge on hg shadow repo.')
863 'Subrepo merge error during local merge on hg shadow repo.')
857 merge_possible = False
864 merge_possible = False
858 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
865 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
859 needs_push = False
866 needs_push = False
860 except RepositoryError as e:
867 except RepositoryError as e:
861 log.exception('Failure when doing local merge on hg shadow repo')
868 log.exception('Failure when doing local merge on hg shadow repo')
862 metadata['unresolved_files'] = 'no unresolved files found'
869 metadata['unresolved_files'] = 'no unresolved files found'
863
870
864 if isinstance(e, UnresolvedFilesInRepo):
871 if isinstance(e, UnresolvedFilesInRepo):
865 all_conflicts = list(e.args[0])
872 all_conflicts = list(e.args[0])
866 max_conflicts = 20
873 max_conflicts = 20
867 if len(all_conflicts) > max_conflicts:
874 if len(all_conflicts) > max_conflicts:
868 conflicts = all_conflicts[:max_conflicts] \
875 conflicts = all_conflicts[:max_conflicts] \
869 + [f'and {len(all_conflicts)-max_conflicts} more.']
876 + [f'and {len(all_conflicts)-max_conflicts} more.']
870 else:
877 else:
871 conflicts = all_conflicts
878 conflicts = all_conflicts
872 metadata['unresolved_files'] = \
879 metadata['unresolved_files'] = \
873 '\n* conflict: ' + \
880 '\n* conflict: ' + \
874 ('\n * conflict: '.join(conflicts))
881 ('\n * conflict: '.join(conflicts))
875
882
876 merge_possible = False
883 merge_possible = False
877 merge_failure_reason = MergeFailureReason.MERGE_FAILED
884 merge_failure_reason = MergeFailureReason.MERGE_FAILED
878 needs_push = False
885 needs_push = False
879
886
880 if merge_possible and not dry_run:
887 if merge_possible and not dry_run:
881 if needs_push:
888 if needs_push:
882 # In case the target is a bookmark, update it, so after pushing
889 # In case the target is a bookmark, update it, so after pushing
883 # the bookmarks is also updated in the target.
890 # the bookmarks is also updated in the target.
884 if target_ref.type == 'book':
891 if target_ref.type == 'book':
885 shadow_repo.bookmark(
892 shadow_repo.bookmark(
886 target_ref.name, revision=merge_commit_id)
893 target_ref.name, revision=merge_commit_id)
887 try:
894 try:
888 shadow_repo_with_hooks = self.get_shadow_instance(
895 shadow_repo_with_hooks = self.get_shadow_instance(
889 shadow_repository_path,
896 shadow_repository_path,
890 enable_hooks=True)
897 enable_hooks=True)
891 # This is the actual merge action, we push from shadow
898 # This is the actual merge action, we push from shadow
892 # into origin.
899 # into origin.
893 # Note: the push_branches option will push any new branch
900 # Note: the push_branches option will push any new branch
894 # defined in the source repository to the target. This may
901 # defined in the source repository to the target. This may
895 # be dangerous as branches are permanent in Mercurial.
902 # be dangerous as branches are permanent in Mercurial.
896 # This feature was requested in issue #441.
903 # This feature was requested in issue #441.
897 shadow_repo_with_hooks._local_push(
904 shadow_repo_with_hooks._local_push(
898 merge_commit_id, self.path, push_branches=True,
905 merge_commit_id, self.path, push_branches=True,
899 enable_hooks=True)
906 enable_hooks=True)
900
907
901 # maybe we also need to push the close_commit_id
908 # maybe we also need to push the close_commit_id
902 if close_commit_id:
909 if close_commit_id:
903 shadow_repo_with_hooks._local_push(
910 shadow_repo_with_hooks._local_push(
904 close_commit_id, self.path, push_branches=True,
911 close_commit_id, self.path, push_branches=True,
905 enable_hooks=True)
912 enable_hooks=True)
906 merge_succeeded = True
913 merge_succeeded = True
907 except RepositoryError:
914 except RepositoryError:
908 log.exception(
915 log.exception(
909 'Failure when doing local push from the shadow '
916 'Failure when doing local push from the shadow '
910 'repository to the target repository at %s.', self.path)
917 'repository to the target repository at %s.', self.path)
911 merge_succeeded = False
918 merge_succeeded = False
912 merge_failure_reason = MergeFailureReason.PUSH_FAILED
919 merge_failure_reason = MergeFailureReason.PUSH_FAILED
913 metadata['target'] = 'hg shadow repo'
920 metadata['target'] = 'hg shadow repo'
914 metadata['merge_commit'] = merge_commit_id
921 metadata['merge_commit'] = merge_commit_id
915 else:
922 else:
916 merge_succeeded = True
923 merge_succeeded = True
917 else:
924 else:
918 merge_succeeded = False
925 merge_succeeded = False
919
926
920 return MergeResponse(
927 return MergeResponse(
921 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
928 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
922 metadata=metadata)
929 metadata=metadata)
923
930
924 def get_shadow_instance(self, shadow_repository_path, enable_hooks=False, cache=False):
931 def get_shadow_instance(self, shadow_repository_path, enable_hooks=False, cache=False):
925 config = self.config.copy()
932 config = self.config.copy()
926 if not enable_hooks:
933 if not enable_hooks:
927 config.clear_section('hooks')
934 config.clear_section('hooks')
928 return MercurialRepository(shadow_repository_path, config, with_wire={"cache": cache})
935 return MercurialRepository(shadow_repository_path, config, with_wire={"cache": cache})
929
936
930 def _validate_pull_reference(self, reference):
937 def _validate_pull_reference(self, reference):
931 if not (reference.name in self.bookmarks or
938 if not (reference.name in self.bookmarks or
932 reference.name in self.branches or
939 reference.name in self.branches or
933 self.get_commit(reference.commit_id)):
940 self.get_commit(reference.commit_id)):
934 raise CommitDoesNotExistError(
941 raise CommitDoesNotExistError(
935 'Unknown branch, bookmark or commit id')
942 'Unknown branch, bookmark or commit id')
936
943
937 def _local_pull(self, repository_path, reference):
944 def _local_pull(self, repository_path, reference):
938 """
945 """
939 Fetch a branch, bookmark or commit from a local repository.
946 Fetch a branch, bookmark or commit from a local repository.
940 """
947 """
941 repository_path = os.path.abspath(repository_path)
948 repository_path = os.path.abspath(repository_path)
942 if repository_path == self.path:
949 if repository_path == self.path:
943 raise ValueError('Cannot pull from the same repository')
950 raise ValueError('Cannot pull from the same repository')
944
951
945 reference_type_to_option_name = {
952 reference_type_to_option_name = {
946 'book': 'bookmark',
953 'book': 'bookmark',
947 'branch': 'branch',
954 'branch': 'branch',
948 }
955 }
949 option_name = reference_type_to_option_name.get(
956 option_name = reference_type_to_option_name.get(
950 reference.type, 'revision')
957 reference.type, 'revision')
951
958
952 if option_name == 'revision':
959 if option_name == 'revision':
953 ref = reference.commit_id
960 ref = reference.commit_id
954 else:
961 else:
955 ref = reference.name
962 ref = reference.name
956
963
957 options = {option_name: [ref]}
964 options = {option_name: [ref]}
958 self._remote.pull_cmd(repository_path, hooks=False, **options)
965 self._remote.pull_cmd(repository_path, hooks=False, **options)
959 self._remote.invalidate_vcs_cache()
966 self._remote.invalidate_vcs_cache()
960
967
961 def bookmark(self, bookmark, revision=None):
968 def bookmark(self, bookmark, revision=None):
962 if isinstance(bookmark, str):
969 if isinstance(bookmark, str):
963 bookmark = safe_str(bookmark)
970 bookmark = safe_str(bookmark)
964 self._remote.bookmark(bookmark, revision=revision)
971 self._remote.bookmark(bookmark, revision=revision)
965 self._remote.invalidate_vcs_cache()
972 self._remote.invalidate_vcs_cache()
966
973
967 def get_path_permissions(self, username):
974 def get_path_permissions(self, username):
968 hgacl_file = os.path.join(self.path, '.hg/hgacl')
975 hgacl_file = os.path.join(self.path, '.hg/hgacl')
969
976
970 def read_patterns(suffix):
977 def read_patterns(suffix):
971 svalue = None
978 svalue = None
972 for section, option in [
979 for section, option in [
973 ('narrowacl', username + suffix),
980 ('narrowacl', username + suffix),
974 ('narrowacl', 'default' + suffix),
981 ('narrowacl', 'default' + suffix),
975 ('narrowhgacl', username + suffix),
982 ('narrowhgacl', username + suffix),
976 ('narrowhgacl', 'default' + suffix)
983 ('narrowhgacl', 'default' + suffix)
977 ]:
984 ]:
978 try:
985 try:
979 svalue = hgacl.get(section, option)
986 svalue = hgacl.get(section, option)
980 break # stop at the first value we find
987 break # stop at the first value we find
981 except configparser.NoOptionError:
988 except configparser.NoOptionError:
982 pass
989 pass
983 if not svalue:
990 if not svalue:
984 return None
991 return None
985 result = ['/']
992 result = ['/']
986 for pattern in svalue.split():
993 for pattern in svalue.split():
987 result.append(pattern)
994 result.append(pattern)
988 if '*' not in pattern and '?' not in pattern:
995 if '*' not in pattern and '?' not in pattern:
989 result.append(pattern + '/*')
996 result.append(pattern + '/*')
990 return result
997 return result
991
998
992 if os.path.exists(hgacl_file):
999 if os.path.exists(hgacl_file):
993 try:
1000 try:
994 hgacl = configparser.RawConfigParser()
1001 hgacl = configparser.RawConfigParser()
995 hgacl.read(hgacl_file)
1002 hgacl.read(hgacl_file)
996
1003
997 includes = read_patterns('.includes')
1004 includes = read_patterns('.includes')
998 excludes = read_patterns('.excludes')
1005 excludes = read_patterns('.excludes')
999 return BasePathPermissionChecker.create_from_patterns(
1006 return BasePathPermissionChecker.create_from_patterns(
1000 includes, excludes)
1007 includes, excludes)
1001 except BaseException as e:
1008 except BaseException as e:
1002 msg = 'Cannot read ACL settings from {} on {}: {}'.format(
1009 msg = 'Cannot read ACL settings from {} on {}: {}'.format(
1003 hgacl_file, self.name, e)
1010 hgacl_file, self.name, e)
1004 raise exceptions.RepositoryRequirementError(msg)
1011 raise exceptions.RepositoryRequirementError(msg)
1005 else:
1012 else:
1006 return None
1013 return None
1007
1014
1008
1015
1009 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
1016 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
1010
1017
1011 def _commit_factory(self, commit_id):
1018 def _commit_factory(self, commit_id):
1012 if isinstance(commit_id, int):
1019 if isinstance(commit_id, int):
1013 return self.repo.get_commit(
1020 return self.repo.get_commit(
1014 commit_idx=commit_id, pre_load=self.pre_load)
1021 commit_idx=commit_id, pre_load=self.pre_load)
1015 else:
1022 else:
1016 return self.repo.get_commit(
1023 return self.repo.get_commit(
1017 commit_id=commit_id, pre_load=self.pre_load)
1024 commit_id=commit_id, pre_load=self.pre_load)
@@ -1,639 +1,638 b''
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 UserForm(localizer, edit=False, available_languages=None, old_data=None):
107 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
108 old_data = old_data or {}
108 old_data = old_data or {}
109 available_languages = available_languages or []
109 available_languages = available_languages or []
110 _ = localizer
110 _ = localizer
111
111
112 class _UserForm(formencode.Schema):
112 class _UserForm(formencode.Schema):
113 allow_extra_fields = True
113 allow_extra_fields = True
114 filter_extra_fields = True
114 filter_extra_fields = True
115 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
115 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
116 v.ValidUsername(localizer, edit, old_data))
116 v.ValidUsername(localizer, edit, old_data))
117 if edit:
117 if edit:
118 new_password = All(
118 new_password = All(
119 v.ValidPassword(localizer),
119 v.ValidPassword(localizer),
120 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
120 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
121 )
121 )
122 password_confirmation = All(
122 password_confirmation = All(
123 v.ValidPassword(localizer),
123 v.ValidPassword(localizer),
124 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
124 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
125 )
125 )
126 admin = v.StringBoolean(if_missing=False)
126 admin = v.StringBoolean(if_missing=False)
127 else:
127 else:
128 password = All(
128 password = All(
129 v.ValidPassword(localizer),
129 v.ValidPassword(localizer),
130 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
130 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
131 )
131 )
132 password_confirmation = All(
132 password_confirmation = All(
133 v.ValidPassword(localizer),
133 v.ValidPassword(localizer),
134 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
134 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
135 )
135 )
136
136
137 password_change = v.StringBoolean(if_missing=False)
137 password_change = v.StringBoolean(if_missing=False)
138 create_repo_group = v.StringBoolean(if_missing=False)
138 create_repo_group = v.StringBoolean(if_missing=False)
139
139
140 active = v.StringBoolean(if_missing=False)
140 active = v.StringBoolean(if_missing=False)
141 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
141 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
142 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
142 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
143 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
143 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
144 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
144 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
145 if_missing='')
145 if_missing='')
146 extern_name = v.UnicodeString(strip=True)
146 extern_name = v.UnicodeString(strip=True)
147 extern_type = v.UnicodeString(strip=True)
147 extern_type = v.UnicodeString(strip=True)
148 language = v.OneOf(available_languages, hideList=False,
148 language = v.OneOf(available_languages, hideList=False,
149 testValueList=True, if_missing=None)
149 testValueList=True, if_missing=None)
150 chained_validators = [v.ValidPasswordsMatch(localizer)]
150 chained_validators = [v.ValidPasswordsMatch(localizer)]
151 return _UserForm
151 return _UserForm
152
152
153
153
154 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
154 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
155 old_data = old_data or {}
155 old_data = old_data or {}
156 _ = localizer
156 _ = localizer
157
157
158 class _UserGroupForm(formencode.Schema):
158 class _UserGroupForm(formencode.Schema):
159 allow_extra_fields = True
159 allow_extra_fields = True
160 filter_extra_fields = True
160 filter_extra_fields = True
161
161
162 users_group_name = All(
162 users_group_name = All(
163 v.UnicodeString(strip=True, min=1, not_empty=True),
163 v.UnicodeString(strip=True, min=1, not_empty=True),
164 v.ValidUserGroup(localizer, edit, old_data)
164 v.ValidUserGroup(localizer, edit, old_data)
165 )
165 )
166 user_group_description = v.UnicodeString(strip=True, min=1,
166 user_group_description = v.UnicodeString(strip=True, min=1,
167 not_empty=False)
167 not_empty=False)
168
168
169 users_group_active = v.StringBoolean(if_missing=False)
169 users_group_active = v.StringBoolean(if_missing=False)
170
170
171 if edit:
171 if edit:
172 # this is user group owner
172 # this is user group owner
173 user = All(
173 user = All(
174 v.UnicodeString(not_empty=True),
174 v.UnicodeString(not_empty=True),
175 v.ValidRepoUser(localizer, allow_disabled))
175 v.ValidRepoUser(localizer, allow_disabled))
176 return _UserGroupForm
176 return _UserGroupForm
177
177
178
178
179 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
179 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
180 can_create_in_root=False, allow_disabled=False):
180 can_create_in_root=False, allow_disabled=False):
181 _ = localizer
181 _ = localizer
182 old_data = old_data or {}
182 old_data = old_data or {}
183 available_groups = available_groups or []
183 available_groups = available_groups or []
184
184
185 class _RepoGroupForm(formencode.Schema):
185 class _RepoGroupForm(formencode.Schema):
186 allow_extra_fields = True
186 allow_extra_fields = True
187 filter_extra_fields = False
187 filter_extra_fields = False
188
188
189 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
189 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
190 v.SlugifyName(localizer),)
190 v.SlugifyName(localizer),)
191 group_description = v.UnicodeString(strip=True, min=1,
191 group_description = v.UnicodeString(strip=True, min=1,
192 not_empty=False)
192 not_empty=False)
193 group_copy_permissions = v.StringBoolean(if_missing=False)
193 group_copy_permissions = v.StringBoolean(if_missing=False)
194
194
195 group_parent_id = v.OneOf(available_groups, hideList=False,
195 group_parent_id = v.OneOf(available_groups, hideList=False,
196 testValueList=True, not_empty=True)
196 testValueList=True, not_empty=True)
197 enable_locking = v.StringBoolean(if_missing=False)
197 enable_locking = v.StringBoolean(if_missing=False)
198 chained_validators = [
198 chained_validators = [
199 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
199 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
200
200
201 if edit:
201 if edit:
202 # this is repo group owner
202 # this is repo group owner
203 user = All(
203 user = All(
204 v.UnicodeString(not_empty=True),
204 v.UnicodeString(not_empty=True),
205 v.ValidRepoUser(localizer, allow_disabled))
205 v.ValidRepoUser(localizer, allow_disabled))
206 return _RepoGroupForm
206 return _RepoGroupForm
207
207
208
208
209 def RegisterForm(localizer, edit=False, old_data=None):
209 def RegisterForm(localizer, edit=False, old_data=None):
210 _ = localizer
210 _ = localizer
211 old_data = old_data or {}
211 old_data = old_data or {}
212
212
213 class _RegisterForm(formencode.Schema):
213 class _RegisterForm(formencode.Schema):
214 allow_extra_fields = True
214 allow_extra_fields = True
215 filter_extra_fields = True
215 filter_extra_fields = True
216 username = All(
216 username = All(
217 v.ValidUsername(localizer, edit, old_data),
217 v.ValidUsername(localizer, edit, old_data),
218 v.UnicodeString(strip=True, min=1, not_empty=True)
218 v.UnicodeString(strip=True, min=1, not_empty=True)
219 )
219 )
220 password = All(
220 password = All(
221 v.ValidPassword(localizer),
221 v.ValidPassword(localizer),
222 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
222 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
223 )
223 )
224 password_confirmation = All(
224 password_confirmation = All(
225 v.ValidPassword(localizer),
225 v.ValidPassword(localizer),
226 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
226 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
227 )
227 )
228 active = v.StringBoolean(if_missing=False)
228 active = v.StringBoolean(if_missing=False)
229 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
229 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
230 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
230 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
231 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
231 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
232
232
233 chained_validators = [v.ValidPasswordsMatch(localizer)]
233 chained_validators = [v.ValidPasswordsMatch(localizer)]
234 return _RegisterForm
234 return _RegisterForm
235
235
236
236
237 def PasswordResetForm(localizer):
237 def PasswordResetForm(localizer):
238 _ = localizer
238 _ = localizer
239
239
240 class _PasswordResetForm(formencode.Schema):
240 class _PasswordResetForm(formencode.Schema):
241 allow_extra_fields = True
241 allow_extra_fields = True
242 filter_extra_fields = True
242 filter_extra_fields = True
243 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
243 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
244 return _PasswordResetForm
244 return _PasswordResetForm
245
245
246
246
247 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
247 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
248 _ = localizer
248 _ = localizer
249 old_data = old_data or {}
249 old_data = old_data or {}
250 repo_groups = repo_groups or []
250 repo_groups = repo_groups or []
251 supported_backends = BACKENDS.keys()
251 supported_backends = BACKENDS.keys()
252
252
253 class _RepoForm(formencode.Schema):
253 class _RepoForm(formencode.Schema):
254 allow_extra_fields = True
254 allow_extra_fields = True
255 filter_extra_fields = False
255 filter_extra_fields = False
256 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
256 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
257 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
257 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
258 repo_group = All(v.CanWriteGroup(localizer, old_data),
258 repo_group = All(v.CanWriteGroup(localizer, old_data),
259 v.OneOf(repo_groups, hideList=True))
259 v.OneOf(repo_groups, hideList=True))
260 repo_type = v.OneOf(supported_backends, required=False,
260 repo_type = v.OneOf(supported_backends, required=False,
261 if_missing=old_data.get('repo_type'))
261 if_missing=old_data.get('repo_type'))
262 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
262 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
263 repo_private = v.StringBoolean(if_missing=False)
263 repo_private = v.StringBoolean(if_missing=False)
264 repo_copy_permissions = v.StringBoolean(if_missing=False)
264 repo_copy_permissions = v.StringBoolean(if_missing=False)
265 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
265 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
266
266
267 repo_enable_statistics = v.StringBoolean(if_missing=False)
267 repo_enable_statistics = v.StringBoolean(if_missing=False)
268 repo_enable_downloads = v.StringBoolean(if_missing=False)
268 repo_enable_downloads = v.StringBoolean(if_missing=False)
269 repo_enable_locking = v.StringBoolean(if_missing=False)
269 repo_enable_locking = v.StringBoolean(if_missing=False)
270
270
271 if edit:
271 if edit:
272 # this is repo owner
272 # this is repo owner
273 user = All(
273 user = All(
274 v.UnicodeString(not_empty=True),
274 v.UnicodeString(not_empty=True),
275 v.ValidRepoUser(localizer, allow_disabled))
275 v.ValidRepoUser(localizer, allow_disabled))
276 clone_uri_change = v.UnicodeString(
276 clone_uri_change = v.UnicodeString(
277 not_empty=False, if_missing=v.Missing)
277 not_empty=False, if_missing=v.Missing)
278
278
279 chained_validators = [v.ValidCloneUri(localizer),
279 chained_validators = [v.ValidCloneUri(localizer),
280 v.ValidRepoName(localizer, edit, old_data)]
280 v.ValidRepoName(localizer, edit, old_data)]
281 return _RepoForm
281 return _RepoForm
282
282
283
283
284 def RepoPermsForm(localizer):
284 def RepoPermsForm(localizer):
285 _ = localizer
285 _ = localizer
286
286
287 class _RepoPermsForm(formencode.Schema):
287 class _RepoPermsForm(formencode.Schema):
288 allow_extra_fields = True
288 allow_extra_fields = True
289 filter_extra_fields = False
289 filter_extra_fields = False
290 chained_validators = [v.ValidPerms(localizer, type_='repo')]
290 chained_validators = [v.ValidPerms(localizer, type_='repo')]
291 return _RepoPermsForm
291 return _RepoPermsForm
292
292
293
293
294 def RepoGroupPermsForm(localizer, valid_recursive_choices):
294 def RepoGroupPermsForm(localizer, valid_recursive_choices):
295 _ = localizer
295 _ = localizer
296
296
297 class _RepoGroupPermsForm(formencode.Schema):
297 class _RepoGroupPermsForm(formencode.Schema):
298 allow_extra_fields = True
298 allow_extra_fields = True
299 filter_extra_fields = False
299 filter_extra_fields = False
300 recursive = v.OneOf(valid_recursive_choices)
300 recursive = v.OneOf(valid_recursive_choices)
301 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
301 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
302 return _RepoGroupPermsForm
302 return _RepoGroupPermsForm
303
303
304
304
305 def UserGroupPermsForm(localizer):
305 def UserGroupPermsForm(localizer):
306 _ = localizer
306 _ = localizer
307
307
308 class _UserPermsForm(formencode.Schema):
308 class _UserPermsForm(formencode.Schema):
309 allow_extra_fields = True
309 allow_extra_fields = True
310 filter_extra_fields = False
310 filter_extra_fields = False
311 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
311 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
312 return _UserPermsForm
312 return _UserPermsForm
313
313
314
314
315 def RepoFieldForm(localizer):
315 def RepoFieldForm(localizer):
316 _ = localizer
316 _ = localizer
317
317
318 class _RepoFieldForm(formencode.Schema):
318 class _RepoFieldForm(formencode.Schema):
319 filter_extra_fields = True
319 filter_extra_fields = True
320 allow_extra_fields = True
320 allow_extra_fields = True
321
321
322 new_field_key = All(v.FieldKey(localizer),
322 new_field_key = All(v.FieldKey(localizer),
323 v.UnicodeString(strip=True, min=3, not_empty=True))
323 v.UnicodeString(strip=True, min=3, not_empty=True))
324 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
324 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
325 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
325 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
326 if_missing='str')
326 if_missing='str')
327 new_field_label = v.UnicodeString(not_empty=False)
327 new_field_label = v.UnicodeString(not_empty=False)
328 new_field_desc = v.UnicodeString(not_empty=False)
328 new_field_desc = v.UnicodeString(not_empty=False)
329 return _RepoFieldForm
329 return _RepoFieldForm
330
330
331
331
332 def RepoForkForm(localizer, edit=False, old_data=None,
332 def RepoForkForm(localizer, edit=False, old_data=None,
333 supported_backends=BACKENDS.keys(), repo_groups=None):
333 supported_backends=BACKENDS.keys(), repo_groups=None):
334 _ = localizer
334 _ = localizer
335 old_data = old_data or {}
335 old_data = old_data or {}
336 repo_groups = repo_groups or []
336 repo_groups = repo_groups or []
337
337
338 class _RepoForkForm(formencode.Schema):
338 class _RepoForkForm(formencode.Schema):
339 allow_extra_fields = True
339 allow_extra_fields = True
340 filter_extra_fields = False
340 filter_extra_fields = False
341 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
341 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
342 v.SlugifyName(localizer))
342 v.SlugifyName(localizer))
343 repo_group = All(v.CanWriteGroup(localizer, ),
343 repo_group = All(v.CanWriteGroup(localizer, ),
344 v.OneOf(repo_groups, hideList=True))
344 v.OneOf(repo_groups, hideList=True))
345 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
345 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
346 description = v.UnicodeString(strip=True, min=1, not_empty=True)
346 description = v.UnicodeString(strip=True, min=1, not_empty=True)
347 private = v.StringBoolean(if_missing=False)
347 private = v.StringBoolean(if_missing=False)
348 copy_permissions = v.StringBoolean(if_missing=False)
348 copy_permissions = v.StringBoolean(if_missing=False)
349 fork_parent_id = v.UnicodeString()
349 fork_parent_id = v.UnicodeString()
350 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
350 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
351 return _RepoForkForm
351 return _RepoForkForm
352
352
353
353
354 def ApplicationSettingsForm(localizer):
354 def ApplicationSettingsForm(localizer):
355 _ = localizer
355 _ = localizer
356
356
357 class _ApplicationSettingsForm(formencode.Schema):
357 class _ApplicationSettingsForm(formencode.Schema):
358 allow_extra_fields = True
358 allow_extra_fields = True
359 filter_extra_fields = False
359 filter_extra_fields = False
360 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
360 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
361 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
361 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
362 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
362 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
363 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
363 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
364 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
364 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
365 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
365 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
366 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
366 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
367 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
367 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
368 return _ApplicationSettingsForm
368 return _ApplicationSettingsForm
369
369
370
370
371 def ApplicationVisualisationForm(localizer):
371 def ApplicationVisualisationForm(localizer):
372 from rhodecode.model.db import Repository
372 from rhodecode.model.db import Repository
373 _ = localizer
373 _ = localizer
374
374
375 class _ApplicationVisualisationForm(formencode.Schema):
375 class _ApplicationVisualisationForm(formencode.Schema):
376 allow_extra_fields = True
376 allow_extra_fields = True
377 filter_extra_fields = False
377 filter_extra_fields = False
378 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
378 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
379 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
379 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
380 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
380 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
381
381
382 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
382 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
383 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
383 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
384 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
384 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
385 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
385 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
386 rhodecode_show_version = v.StringBoolean(if_missing=False)
386 rhodecode_show_version = v.StringBoolean(if_missing=False)
387 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
387 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
388 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
388 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
389 rhodecode_gravatar_url = v.UnicodeString(min=3)
389 rhodecode_gravatar_url = v.UnicodeString(min=3)
390 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
390 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
391 rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID)
391 rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID)
392 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
392 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
393 rhodecode_support_url = v.UnicodeString()
393 rhodecode_support_url = v.UnicodeString()
394 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
394 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
395 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
395 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
396 return _ApplicationVisualisationForm
396 return _ApplicationVisualisationForm
397
397
398
398
399 class _BaseVcsSettingsForm(formencode.Schema):
399 class _BaseVcsSettingsForm(formencode.Schema):
400
400
401 allow_extra_fields = True
401 allow_extra_fields = True
402 filter_extra_fields = False
402 filter_extra_fields = False
403 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
403 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
404 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
404 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
405 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
405 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
406
406
407 # PR/Code-review
407 # PR/Code-review
408 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
408 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
409 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
409 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
410
410
411 # hg
411 # hg
412 extensions_largefiles = v.StringBoolean(if_missing=False)
412 extensions_largefiles = v.StringBoolean(if_missing=False)
413 extensions_evolve = v.StringBoolean(if_missing=False)
413 extensions_evolve = v.StringBoolean(if_missing=False)
414 phases_publish = v.StringBoolean(if_missing=False)
414 phases_publish = v.StringBoolean(if_missing=False)
415
415
416 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
416 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
417 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
417 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
418
418
419 # git
419 # git
420 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
420 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
421 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
421 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
422 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
422 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
423
423
424 # svn
424 # svn
425 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
425 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
426 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
426 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
427
427
428 # cache
428 # cache
429 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
429 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
430
430
431
431
432 def ApplicationUiSettingsForm(localizer):
432 def ApplicationUiSettingsForm(localizer):
433 _ = localizer
433 _ = localizer
434
434
435 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
435 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
436 web_push_ssl = v.StringBoolean(if_missing=False)
436 web_push_ssl = v.StringBoolean(if_missing=False)
437 paths_root_path = All(
437 paths_root_path = All(
438 v.ValidPath(localizer),
438 v.ValidPath(localizer),
439 v.UnicodeString(strip=True, min=1, not_empty=True)
439 v.UnicodeString(strip=True, min=1, not_empty=True)
440 )
440 )
441 largefiles_usercache = All(
441 largefiles_usercache = All(
442 v.ValidPath(localizer),
442 v.ValidPath(localizer),
443 v.UnicodeString(strip=True, min=2, not_empty=True))
443 v.UnicodeString(strip=True, min=2, not_empty=True))
444 vcs_git_lfs_store_location = All(
444 vcs_git_lfs_store_location = All(
445 v.ValidPath(localizer),
445 v.ValidPath(localizer),
446 v.UnicodeString(strip=True, min=2, not_empty=True))
446 v.UnicodeString(strip=True, min=2, not_empty=True))
447 extensions_hgsubversion = v.StringBoolean(if_missing=False)
448 extensions_hggit = v.StringBoolean(if_missing=False)
447 extensions_hggit = v.StringBoolean(if_missing=False)
449 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
448 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
450 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
449 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
451 return _ApplicationUiSettingsForm
450 return _ApplicationUiSettingsForm
452
451
453
452
454 def RepoVcsSettingsForm(localizer, repo_name):
453 def RepoVcsSettingsForm(localizer, repo_name):
455 _ = localizer
454 _ = localizer
456
455
457 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
456 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
458 inherit_global_settings = v.StringBoolean(if_missing=False)
457 inherit_global_settings = v.StringBoolean(if_missing=False)
459 new_svn_branch = v.ValidSvnPattern(localizer,
458 new_svn_branch = v.ValidSvnPattern(localizer,
460 section='vcs_svn_branch', repo_name=repo_name)
459 section='vcs_svn_branch', repo_name=repo_name)
461 new_svn_tag = v.ValidSvnPattern(localizer,
460 new_svn_tag = v.ValidSvnPattern(localizer,
462 section='vcs_svn_tag', repo_name=repo_name)
461 section='vcs_svn_tag', repo_name=repo_name)
463 return _RepoVcsSettingsForm
462 return _RepoVcsSettingsForm
464
463
465
464
466 def LabsSettingsForm(localizer):
465 def LabsSettingsForm(localizer):
467 _ = localizer
466 _ = localizer
468
467
469 class _LabSettingsForm(formencode.Schema):
468 class _LabSettingsForm(formencode.Schema):
470 allow_extra_fields = True
469 allow_extra_fields = True
471 filter_extra_fields = False
470 filter_extra_fields = False
472 return _LabSettingsForm
471 return _LabSettingsForm
473
472
474
473
475 def ApplicationPermissionsForm(
474 def ApplicationPermissionsForm(
476 localizer, register_choices, password_reset_choices,
475 localizer, register_choices, password_reset_choices,
477 extern_activate_choices):
476 extern_activate_choices):
478 _ = localizer
477 _ = localizer
479
478
480 class _DefaultPermissionsForm(formencode.Schema):
479 class _DefaultPermissionsForm(formencode.Schema):
481 allow_extra_fields = True
480 allow_extra_fields = True
482 filter_extra_fields = True
481 filter_extra_fields = True
483
482
484 anonymous = v.StringBoolean(if_missing=False)
483 anonymous = v.StringBoolean(if_missing=False)
485 default_register = v.OneOf(register_choices)
484 default_register = v.OneOf(register_choices)
486 default_register_message = v.UnicodeString()
485 default_register_message = v.UnicodeString()
487 default_password_reset = v.OneOf(password_reset_choices)
486 default_password_reset = v.OneOf(password_reset_choices)
488 default_extern_activate = v.OneOf(extern_activate_choices)
487 default_extern_activate = v.OneOf(extern_activate_choices)
489 return _DefaultPermissionsForm
488 return _DefaultPermissionsForm
490
489
491
490
492 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
491 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
493 user_group_perms_choices):
492 user_group_perms_choices):
494 _ = localizer
493 _ = localizer
495
494
496 class _ObjectPermissionsForm(formencode.Schema):
495 class _ObjectPermissionsForm(formencode.Schema):
497 allow_extra_fields = True
496 allow_extra_fields = True
498 filter_extra_fields = True
497 filter_extra_fields = True
499 overwrite_default_repo = v.StringBoolean(if_missing=False)
498 overwrite_default_repo = v.StringBoolean(if_missing=False)
500 overwrite_default_group = v.StringBoolean(if_missing=False)
499 overwrite_default_group = v.StringBoolean(if_missing=False)
501 overwrite_default_user_group = v.StringBoolean(if_missing=False)
500 overwrite_default_user_group = v.StringBoolean(if_missing=False)
502
501
503 default_repo_perm = v.OneOf(repo_perms_choices)
502 default_repo_perm = v.OneOf(repo_perms_choices)
504 default_group_perm = v.OneOf(group_perms_choices)
503 default_group_perm = v.OneOf(group_perms_choices)
505 default_user_group_perm = v.OneOf(user_group_perms_choices)
504 default_user_group_perm = v.OneOf(user_group_perms_choices)
506
505
507 return _ObjectPermissionsForm
506 return _ObjectPermissionsForm
508
507
509
508
510 def BranchPermissionsForm(localizer, branch_perms_choices):
509 def BranchPermissionsForm(localizer, branch_perms_choices):
511 _ = localizer
510 _ = localizer
512
511
513 class _BranchPermissionsForm(formencode.Schema):
512 class _BranchPermissionsForm(formencode.Schema):
514 allow_extra_fields = True
513 allow_extra_fields = True
515 filter_extra_fields = True
514 filter_extra_fields = True
516 overwrite_default_branch = v.StringBoolean(if_missing=False)
515 overwrite_default_branch = v.StringBoolean(if_missing=False)
517 default_branch_perm = v.OneOf(branch_perms_choices)
516 default_branch_perm = v.OneOf(branch_perms_choices)
518
517
519 return _BranchPermissionsForm
518 return _BranchPermissionsForm
520
519
521
520
522 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
521 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
523 repo_group_create_choices, user_group_create_choices,
522 repo_group_create_choices, user_group_create_choices,
524 fork_choices, inherit_default_permissions_choices):
523 fork_choices, inherit_default_permissions_choices):
525 _ = localizer
524 _ = localizer
526
525
527 class _DefaultPermissionsForm(formencode.Schema):
526 class _DefaultPermissionsForm(formencode.Schema):
528 allow_extra_fields = True
527 allow_extra_fields = True
529 filter_extra_fields = True
528 filter_extra_fields = True
530
529
531 anonymous = v.StringBoolean(if_missing=False)
530 anonymous = v.StringBoolean(if_missing=False)
532
531
533 default_repo_create = v.OneOf(create_choices)
532 default_repo_create = v.OneOf(create_choices)
534 default_repo_create_on_write = v.OneOf(create_on_write_choices)
533 default_repo_create_on_write = v.OneOf(create_on_write_choices)
535 default_user_group_create = v.OneOf(user_group_create_choices)
534 default_user_group_create = v.OneOf(user_group_create_choices)
536 default_repo_group_create = v.OneOf(repo_group_create_choices)
535 default_repo_group_create = v.OneOf(repo_group_create_choices)
537 default_fork_create = v.OneOf(fork_choices)
536 default_fork_create = v.OneOf(fork_choices)
538 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
537 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
539 return _DefaultPermissionsForm
538 return _DefaultPermissionsForm
540
539
541
540
542 def UserIndividualPermissionsForm(localizer):
541 def UserIndividualPermissionsForm(localizer):
543 _ = localizer
542 _ = localizer
544
543
545 class _DefaultPermissionsForm(formencode.Schema):
544 class _DefaultPermissionsForm(formencode.Schema):
546 allow_extra_fields = True
545 allow_extra_fields = True
547 filter_extra_fields = True
546 filter_extra_fields = True
548
547
549 inherit_default_permissions = v.StringBoolean(if_missing=False)
548 inherit_default_permissions = v.StringBoolean(if_missing=False)
550 return _DefaultPermissionsForm
549 return _DefaultPermissionsForm
551
550
552
551
553 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
552 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
554 _ = localizer
553 _ = localizer
555 old_data = old_data or {}
554 old_data = old_data or {}
556
555
557 class _DefaultsForm(formencode.Schema):
556 class _DefaultsForm(formencode.Schema):
558 allow_extra_fields = True
557 allow_extra_fields = True
559 filter_extra_fields = True
558 filter_extra_fields = True
560 default_repo_type = v.OneOf(supported_backends)
559 default_repo_type = v.OneOf(supported_backends)
561 default_repo_private = v.StringBoolean(if_missing=False)
560 default_repo_private = v.StringBoolean(if_missing=False)
562 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
561 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
563 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
562 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
564 default_repo_enable_locking = v.StringBoolean(if_missing=False)
563 default_repo_enable_locking = v.StringBoolean(if_missing=False)
565 return _DefaultsForm
564 return _DefaultsForm
566
565
567
566
568 def AuthSettingsForm(localizer):
567 def AuthSettingsForm(localizer):
569 _ = localizer
568 _ = localizer
570
569
571 class _AuthSettingsForm(formencode.Schema):
570 class _AuthSettingsForm(formencode.Schema):
572 allow_extra_fields = True
571 allow_extra_fields = True
573 filter_extra_fields = True
572 filter_extra_fields = True
574 auth_plugins = All(v.ValidAuthPlugins(localizer),
573 auth_plugins = All(v.ValidAuthPlugins(localizer),
575 v.UniqueListFromString(localizer)(not_empty=True))
574 v.UniqueListFromString(localizer)(not_empty=True))
576 return _AuthSettingsForm
575 return _AuthSettingsForm
577
576
578
577
579 def UserExtraEmailForm(localizer):
578 def UserExtraEmailForm(localizer):
580 _ = localizer
579 _ = localizer
581
580
582 class _UserExtraEmailForm(formencode.Schema):
581 class _UserExtraEmailForm(formencode.Schema):
583 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
582 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
584 return _UserExtraEmailForm
583 return _UserExtraEmailForm
585
584
586
585
587 def UserExtraIpForm(localizer):
586 def UserExtraIpForm(localizer):
588 _ = localizer
587 _ = localizer
589
588
590 class _UserExtraIpForm(formencode.Schema):
589 class _UserExtraIpForm(formencode.Schema):
591 ip = v.ValidIp(localizer)(not_empty=True)
590 ip = v.ValidIp(localizer)(not_empty=True)
592 return _UserExtraIpForm
591 return _UserExtraIpForm
593
592
594
593
595 def PullRequestForm(localizer, repo_id):
594 def PullRequestForm(localizer, repo_id):
596 _ = localizer
595 _ = localizer
597
596
598 class ReviewerForm(formencode.Schema):
597 class ReviewerForm(formencode.Schema):
599 user_id = v.Int(not_empty=True)
598 user_id = v.Int(not_empty=True)
600 reasons = All()
599 reasons = All()
601 rules = All(v.UniqueList(localizer, convert=int)())
600 rules = All(v.UniqueList(localizer, convert=int)())
602 mandatory = v.StringBoolean()
601 mandatory = v.StringBoolean()
603 role = v.String(if_missing='reviewer')
602 role = v.String(if_missing='reviewer')
604
603
605 class ObserverForm(formencode.Schema):
604 class ObserverForm(formencode.Schema):
606 user_id = v.Int(not_empty=True)
605 user_id = v.Int(not_empty=True)
607 reasons = All()
606 reasons = All()
608 rules = All(v.UniqueList(localizer, convert=int)())
607 rules = All(v.UniqueList(localizer, convert=int)())
609 mandatory = v.StringBoolean()
608 mandatory = v.StringBoolean()
610 role = v.String(if_missing='observer')
609 role = v.String(if_missing='observer')
611
610
612 class _PullRequestForm(formencode.Schema):
611 class _PullRequestForm(formencode.Schema):
613 allow_extra_fields = True
612 allow_extra_fields = True
614 filter_extra_fields = True
613 filter_extra_fields = True
615
614
616 common_ancestor = v.UnicodeString(strip=True, required=True)
615 common_ancestor = v.UnicodeString(strip=True, required=True)
617 source_repo = v.UnicodeString(strip=True, required=True)
616 source_repo = v.UnicodeString(strip=True, required=True)
618 source_ref = v.UnicodeString(strip=True, required=True)
617 source_ref = v.UnicodeString(strip=True, required=True)
619 target_repo = v.UnicodeString(strip=True, required=True)
618 target_repo = v.UnicodeString(strip=True, required=True)
620 target_ref = v.UnicodeString(strip=True, required=True)
619 target_ref = v.UnicodeString(strip=True, required=True)
621 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
620 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
622 v.UniqueList(localizer)(not_empty=True))
621 v.UniqueList(localizer)(not_empty=True))
623 review_members = formencode.ForEach(ReviewerForm())
622 review_members = formencode.ForEach(ReviewerForm())
624 observer_members = formencode.ForEach(ObserverForm())
623 observer_members = formencode.ForEach(ObserverForm())
625 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
624 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
626 pullrequest_desc = v.UnicodeString(strip=True, required=False)
625 pullrequest_desc = v.UnicodeString(strip=True, required=False)
627 description_renderer = v.UnicodeString(strip=True, required=False)
626 description_renderer = v.UnicodeString(strip=True, required=False)
628
627
629 return _PullRequestForm
628 return _PullRequestForm
630
629
631
630
632 def IssueTrackerPatternsForm(localizer):
631 def IssueTrackerPatternsForm(localizer):
633 _ = localizer
632 _ = localizer
634
633
635 class _IssueTrackerPatternsForm(formencode.Schema):
634 class _IssueTrackerPatternsForm(formencode.Schema):
636 allow_extra_fields = True
635 allow_extra_fields = True
637 filter_extra_fields = False
636 filter_extra_fields = False
638 chained_validators = [v.ValidPattern(localizer)]
637 chained_validators = [v.ValidPattern(localizer)]
639 return _IssueTrackerPatternsForm
638 return _IssueTrackerPatternsForm
@@ -1,927 +1,925 b''
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 'hg_use_rebase_for_merging',
464 'hg_use_rebase_for_merging',
465 'hg_close_branch_before_merging',
465 'hg_close_branch_before_merging',
466 'git_use_rebase_for_merging',
466 'git_use_rebase_for_merging',
467 'git_close_branch_before_merging',
467 'git_close_branch_before_merging',
468 'diff_cache',
468 'diff_cache',
469 )
469 )
470
470
471 HOOKS_SETTINGS = (
471 HOOKS_SETTINGS = (
472 ('hooks', 'changegroup.repo_size'),
472 ('hooks', 'changegroup.repo_size'),
473 ('hooks', 'changegroup.push_logger'),
473 ('hooks', 'changegroup.push_logger'),
474 ('hooks', 'outgoing.pull_logger'),
474 ('hooks', 'outgoing.pull_logger'),
475 )
475 )
476 HG_SETTINGS = (
476 HG_SETTINGS = (
477 ('extensions', 'largefiles'),
477 ('extensions', 'largefiles'),
478 ('phases', 'publish'),
478 ('phases', 'publish'),
479 ('extensions', 'evolve'),
479 ('extensions', 'evolve'),
480 ('extensions', 'topic'),
480 ('extensions', 'topic'),
481 ('experimental', 'evolution'),
481 ('experimental', 'evolution'),
482 ('experimental', 'evolution.exchange'),
482 ('experimental', 'evolution.exchange'),
483 )
483 )
484 GIT_SETTINGS = (
484 GIT_SETTINGS = (
485 ('vcs_git_lfs', 'enabled'),
485 ('vcs_git_lfs', 'enabled'),
486 )
486 )
487 GLOBAL_HG_SETTINGS = (
487 GLOBAL_HG_SETTINGS = (
488 ('extensions', 'largefiles'),
488 ('extensions', 'largefiles'),
489 ('largefiles', 'usercache'),
489 ('largefiles', 'usercache'),
490 ('phases', 'publish'),
490 ('phases', 'publish'),
491 ('extensions', 'hgsubversion'),
492 ('extensions', 'evolve'),
491 ('extensions', 'evolve'),
493 ('extensions', 'topic'),
492 ('extensions', 'topic'),
494 ('experimental', 'evolution'),
493 ('experimental', 'evolution'),
495 ('experimental', 'evolution.exchange'),
494 ('experimental', 'evolution.exchange'),
496 )
495 )
497
496
498 GLOBAL_GIT_SETTINGS = (
497 GLOBAL_GIT_SETTINGS = (
499 ('vcs_git_lfs', 'enabled'),
498 ('vcs_git_lfs', 'enabled'),
500 ('vcs_git_lfs', 'store_location')
499 ('vcs_git_lfs', 'store_location')
501 )
500 )
502
501
503 GLOBAL_SVN_SETTINGS = (
502 GLOBAL_SVN_SETTINGS = (
504 ('vcs_svn_proxy', 'http_requests_enabled'),
503 ('vcs_svn_proxy', 'http_requests_enabled'),
505 ('vcs_svn_proxy', 'http_server_url')
504 ('vcs_svn_proxy', 'http_server_url')
506 )
505 )
507
506
508 SVN_BRANCH_SECTION = 'vcs_svn_branch'
507 SVN_BRANCH_SECTION = 'vcs_svn_branch'
509 SVN_TAG_SECTION = 'vcs_svn_tag'
508 SVN_TAG_SECTION = 'vcs_svn_tag'
510 SSL_SETTING = ('web', 'push_ssl')
509 SSL_SETTING = ('web', 'push_ssl')
511 PATH_SETTING = ('paths', '/')
510 PATH_SETTING = ('paths', '/')
512
511
513 def __init__(self, sa=None, repo=None):
512 def __init__(self, sa=None, repo=None):
514 self.global_settings = SettingsModel(sa=sa)
513 self.global_settings = SettingsModel(sa=sa)
515 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
514 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
516 self._ui_settings = (
515 self._ui_settings = (
517 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
516 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
518 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
517 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
519
518
520 @property
519 @property
521 @assert_repo_settings
520 @assert_repo_settings
522 def inherit_global_settings(self):
521 def inherit_global_settings(self):
523 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
522 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
524 return setting.app_settings_value if setting else True
523 return setting.app_settings_value if setting else True
525
524
526 @inherit_global_settings.setter
525 @inherit_global_settings.setter
527 @assert_repo_settings
526 @assert_repo_settings
528 def inherit_global_settings(self, value):
527 def inherit_global_settings(self, value):
529 self.repo_settings.create_or_update_setting(
528 self.repo_settings.create_or_update_setting(
530 self.INHERIT_SETTINGS, value, type_='bool')
529 self.INHERIT_SETTINGS, value, type_='bool')
531
530
532 def get_keyname(self, key_name, prefix='rhodecode_'):
531 def get_keyname(self, key_name, prefix='rhodecode_'):
533 return f'{prefix}{key_name}'
532 return f'{prefix}{key_name}'
534
533
535 def get_global_svn_branch_patterns(self):
534 def get_global_svn_branch_patterns(self):
536 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
535 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
537
536
538 @assert_repo_settings
537 @assert_repo_settings
539 def get_repo_svn_branch_patterns(self):
538 def get_repo_svn_branch_patterns(self):
540 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
539 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
541
540
542 def get_global_svn_tag_patterns(self):
541 def get_global_svn_tag_patterns(self):
543 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
542 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
544
543
545 @assert_repo_settings
544 @assert_repo_settings
546 def get_repo_svn_tag_patterns(self):
545 def get_repo_svn_tag_patterns(self):
547 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
546 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
548
547
549 def get_global_settings(self):
548 def get_global_settings(self):
550 return self._collect_all_settings(global_=True)
549 return self._collect_all_settings(global_=True)
551
550
552 @assert_repo_settings
551 @assert_repo_settings
553 def get_repo_settings(self):
552 def get_repo_settings(self):
554 return self._collect_all_settings(global_=False)
553 return self._collect_all_settings(global_=False)
555
554
556 @assert_repo_settings
555 @assert_repo_settings
557 def get_repo_settings_inherited(self):
556 def get_repo_settings_inherited(self):
558 global_settings = self.get_global_settings()
557 global_settings = self.get_global_settings()
559 global_settings.update(self.get_repo_settings())
558 global_settings.update(self.get_repo_settings())
560 return global_settings
559 return global_settings
561
560
562 @assert_repo_settings
561 @assert_repo_settings
563 def create_or_update_repo_settings(
562 def create_or_update_repo_settings(
564 self, data, inherit_global_settings=False):
563 self, data, inherit_global_settings=False):
565 from rhodecode.model.scm import ScmModel
564 from rhodecode.model.scm import ScmModel
566
565
567 self.inherit_global_settings = inherit_global_settings
566 self.inherit_global_settings = inherit_global_settings
568
567
569 repo = self.repo_settings.get_repo()
568 repo = self.repo_settings.get_repo()
570 if not inherit_global_settings:
569 if not inherit_global_settings:
571 if repo.repo_type == 'svn':
570 if repo.repo_type == 'svn':
572 self.create_repo_svn_settings(data)
571 self.create_repo_svn_settings(data)
573 else:
572 else:
574 self.create_or_update_repo_hook_settings(data)
573 self.create_or_update_repo_hook_settings(data)
575 self.create_or_update_repo_pr_settings(data)
574 self.create_or_update_repo_pr_settings(data)
576
575
577 if repo.repo_type == 'hg':
576 if repo.repo_type == 'hg':
578 self.create_or_update_repo_hg_settings(data)
577 self.create_or_update_repo_hg_settings(data)
579
578
580 if repo.repo_type == 'git':
579 if repo.repo_type == 'git':
581 self.create_or_update_repo_git_settings(data)
580 self.create_or_update_repo_git_settings(data)
582
581
583 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
582 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
584
583
585 @assert_repo_settings
584 @assert_repo_settings
586 def create_or_update_repo_hook_settings(self, data):
585 def create_or_update_repo_hook_settings(self, data):
587 for section, key in self.HOOKS_SETTINGS:
586 for section, key in self.HOOKS_SETTINGS:
588 data_key = self._get_form_ui_key(section, key)
587 data_key = self._get_form_ui_key(section, key)
589 if data_key not in data:
588 if data_key not in data:
590 raise ValueError(
589 raise ValueError(
591 f'The given data does not contain {data_key} key')
590 f'The given data does not contain {data_key} key')
592
591
593 active = data.get(data_key)
592 active = data.get(data_key)
594 repo_setting = self.repo_settings.get_ui_by_section_and_key(
593 repo_setting = self.repo_settings.get_ui_by_section_and_key(
595 section, key)
594 section, key)
596 if not repo_setting:
595 if not repo_setting:
597 global_setting = self.global_settings.\
596 global_setting = self.global_settings.\
598 get_ui_by_section_and_key(section, key)
597 get_ui_by_section_and_key(section, key)
599 self.repo_settings.create_ui_section_value(
598 self.repo_settings.create_ui_section_value(
600 section, global_setting.ui_value, key=key, active=active)
599 section, global_setting.ui_value, key=key, active=active)
601 else:
600 else:
602 repo_setting.ui_active = active
601 repo_setting.ui_active = active
603 Session().add(repo_setting)
602 Session().add(repo_setting)
604
603
605 def update_global_hook_settings(self, data):
604 def update_global_hook_settings(self, data):
606 for section, key in self.HOOKS_SETTINGS:
605 for section, key in self.HOOKS_SETTINGS:
607 data_key = self._get_form_ui_key(section, key)
606 data_key = self._get_form_ui_key(section, key)
608 if data_key not in data:
607 if data_key not in data:
609 raise ValueError(
608 raise ValueError(
610 f'The given data does not contain {data_key} key')
609 f'The given data does not contain {data_key} key')
611 active = data.get(data_key)
610 active = data.get(data_key)
612 repo_setting = self.global_settings.get_ui_by_section_and_key(
611 repo_setting = self.global_settings.get_ui_by_section_and_key(
613 section, key)
612 section, key)
614 repo_setting.ui_active = active
613 repo_setting.ui_active = active
615 Session().add(repo_setting)
614 Session().add(repo_setting)
616
615
617 @assert_repo_settings
616 @assert_repo_settings
618 def create_or_update_repo_pr_settings(self, data):
617 def create_or_update_repo_pr_settings(self, data):
619 return self._create_or_update_general_settings(
618 return self._create_or_update_general_settings(
620 self.repo_settings, data)
619 self.repo_settings, data)
621
620
622 def create_or_update_global_pr_settings(self, data):
621 def create_or_update_global_pr_settings(self, data):
623 return self._create_or_update_general_settings(
622 return self._create_or_update_general_settings(
624 self.global_settings, data)
623 self.global_settings, data)
625
624
626 @assert_repo_settings
625 @assert_repo_settings
627 def create_repo_svn_settings(self, data):
626 def create_repo_svn_settings(self, data):
628 return self._create_svn_settings(self.repo_settings, data)
627 return self._create_svn_settings(self.repo_settings, data)
629
628
630 def _set_evolution(self, settings, is_enabled):
629 def _set_evolution(self, settings, is_enabled):
631 if is_enabled:
630 if is_enabled:
632 # if evolve is active set evolution=all
631 # if evolve is active set evolution=all
633
632
634 self._create_or_update_ui(
633 self._create_or_update_ui(
635 settings, *('experimental', 'evolution'), value='all',
634 settings, *('experimental', 'evolution'), value='all',
636 active=True)
635 active=True)
637 self._create_or_update_ui(
636 self._create_or_update_ui(
638 settings, *('experimental', 'evolution.exchange'), value='yes',
637 settings, *('experimental', 'evolution.exchange'), value='yes',
639 active=True)
638 active=True)
640 # if evolve is active set topics server support
639 # if evolve is active set topics server support
641 self._create_or_update_ui(
640 self._create_or_update_ui(
642 settings, *('extensions', 'topic'), value='',
641 settings, *('extensions', 'topic'), value='',
643 active=True)
642 active=True)
644
643
645 else:
644 else:
646 self._create_or_update_ui(
645 self._create_or_update_ui(
647 settings, *('experimental', 'evolution'), value='',
646 settings, *('experimental', 'evolution'), value='',
648 active=False)
647 active=False)
649 self._create_or_update_ui(
648 self._create_or_update_ui(
650 settings, *('experimental', 'evolution.exchange'), value='no',
649 settings, *('experimental', 'evolution.exchange'), value='no',
651 active=False)
650 active=False)
652 self._create_or_update_ui(
651 self._create_or_update_ui(
653 settings, *('extensions', 'topic'), value='',
652 settings, *('extensions', 'topic'), value='',
654 active=False)
653 active=False)
655
654
656 @assert_repo_settings
655 @assert_repo_settings
657 def create_or_update_repo_hg_settings(self, data):
656 def create_or_update_repo_hg_settings(self, data):
658 largefiles, phases, evolve = \
657 largefiles, phases, evolve = \
659 self.HG_SETTINGS[:3]
658 self.HG_SETTINGS[:3]
660 largefiles_key, phases_key, evolve_key = \
659 largefiles_key, phases_key, evolve_key = \
661 self._get_settings_keys(self.HG_SETTINGS[:3], data)
660 self._get_settings_keys(self.HG_SETTINGS[:3], data)
662
661
663 self._create_or_update_ui(
662 self._create_or_update_ui(
664 self.repo_settings, *largefiles, value='',
663 self.repo_settings, *largefiles, value='',
665 active=data[largefiles_key])
664 active=data[largefiles_key])
666 self._create_or_update_ui(
665 self._create_or_update_ui(
667 self.repo_settings, *evolve, value='',
666 self.repo_settings, *evolve, value='',
668 active=data[evolve_key])
667 active=data[evolve_key])
669 self._set_evolution(self.repo_settings, is_enabled=data[evolve_key])
668 self._set_evolution(self.repo_settings, is_enabled=data[evolve_key])
670
669
671 self._create_or_update_ui(
670 self._create_or_update_ui(
672 self.repo_settings, *phases, value=safe_str(data[phases_key]))
671 self.repo_settings, *phases, value=safe_str(data[phases_key]))
673
672
674 def create_or_update_global_hg_settings(self, data):
673 def create_or_update_global_hg_settings(self, data):
675 largefiles, largefiles_store, phases, hgsubversion, evolve \
674 opts_len = 4
676 = self.GLOBAL_HG_SETTINGS[:5]
675 largefiles, largefiles_store, phases, evolve \
677 largefiles_key, largefiles_store_key, phases_key, subversion_key, evolve_key \
676 = self.GLOBAL_HG_SETTINGS[:opts_len]
678 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS[:5], data)
677 largefiles_key, largefiles_store_key, phases_key, evolve_key \
678 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS[:opts_len], data)
679
679
680 self._create_or_update_ui(
680 self._create_or_update_ui(
681 self.global_settings, *largefiles, value='',
681 self.global_settings, *largefiles, value='',
682 active=data[largefiles_key])
682 active=data[largefiles_key])
683 self._create_or_update_ui(
683 self._create_or_update_ui(
684 self.global_settings, *largefiles_store, value=data[largefiles_store_key])
684 self.global_settings, *largefiles_store, value=data[largefiles_store_key])
685 self._create_or_update_ui(
685 self._create_or_update_ui(
686 self.global_settings, *phases, value=safe_str(data[phases_key]))
686 self.global_settings, *phases, value=safe_str(data[phases_key]))
687 self._create_or_update_ui(
687 self._create_or_update_ui(
688 self.global_settings, *hgsubversion, active=data[subversion_key])
689 self._create_or_update_ui(
690 self.global_settings, *evolve, value='',
688 self.global_settings, *evolve, value='',
691 active=data[evolve_key])
689 active=data[evolve_key])
692 self._set_evolution(self.global_settings, is_enabled=data[evolve_key])
690 self._set_evolution(self.global_settings, is_enabled=data[evolve_key])
693
691
694 def create_or_update_repo_git_settings(self, data):
692 def create_or_update_repo_git_settings(self, data):
695 # NOTE(marcink): # comma makes unpack work properly
693 # NOTE(marcink): # comma makes unpack work properly
696 lfs_enabled, \
694 lfs_enabled, \
697 = self.GIT_SETTINGS
695 = self.GIT_SETTINGS
698
696
699 lfs_enabled_key, \
697 lfs_enabled_key, \
700 = self._get_settings_keys(self.GIT_SETTINGS, data)
698 = self._get_settings_keys(self.GIT_SETTINGS, data)
701
699
702 self._create_or_update_ui(
700 self._create_or_update_ui(
703 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
701 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
704 active=data[lfs_enabled_key])
702 active=data[lfs_enabled_key])
705
703
706 def create_or_update_global_git_settings(self, data):
704 def create_or_update_global_git_settings(self, data):
707 lfs_enabled, lfs_store_location \
705 lfs_enabled, lfs_store_location \
708 = self.GLOBAL_GIT_SETTINGS
706 = self.GLOBAL_GIT_SETTINGS
709 lfs_enabled_key, lfs_store_location_key \
707 lfs_enabled_key, lfs_store_location_key \
710 = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)
708 = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)
711
709
712 self._create_or_update_ui(
710 self._create_or_update_ui(
713 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
711 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
714 active=data[lfs_enabled_key])
712 active=data[lfs_enabled_key])
715 self._create_or_update_ui(
713 self._create_or_update_ui(
716 self.global_settings, *lfs_store_location,
714 self.global_settings, *lfs_store_location,
717 value=data[lfs_store_location_key])
715 value=data[lfs_store_location_key])
718
716
719 def create_or_update_global_svn_settings(self, data):
717 def create_or_update_global_svn_settings(self, data):
720 # branch/tags patterns
718 # branch/tags patterns
721 self._create_svn_settings(self.global_settings, data)
719 self._create_svn_settings(self.global_settings, data)
722
720
723 http_requests_enabled, http_server_url = self.GLOBAL_SVN_SETTINGS
721 http_requests_enabled, http_server_url = self.GLOBAL_SVN_SETTINGS
724 http_requests_enabled_key, http_server_url_key = self._get_settings_keys(
722 http_requests_enabled_key, http_server_url_key = self._get_settings_keys(
725 self.GLOBAL_SVN_SETTINGS, data)
723 self.GLOBAL_SVN_SETTINGS, data)
726
724
727 self._create_or_update_ui(
725 self._create_or_update_ui(
728 self.global_settings, *http_requests_enabled,
726 self.global_settings, *http_requests_enabled,
729 value=safe_str(data[http_requests_enabled_key]))
727 value=safe_str(data[http_requests_enabled_key]))
730 self._create_or_update_ui(
728 self._create_or_update_ui(
731 self.global_settings, *http_server_url,
729 self.global_settings, *http_server_url,
732 value=data[http_server_url_key])
730 value=data[http_server_url_key])
733
731
734 def update_global_ssl_setting(self, value):
732 def update_global_ssl_setting(self, value):
735 self._create_or_update_ui(
733 self._create_or_update_ui(
736 self.global_settings, *self.SSL_SETTING, value=value)
734 self.global_settings, *self.SSL_SETTING, value=value)
737
735
738 def update_global_path_setting(self, value):
736 def update_global_path_setting(self, value):
739 self._create_or_update_ui(
737 self._create_or_update_ui(
740 self.global_settings, *self.PATH_SETTING, value=value)
738 self.global_settings, *self.PATH_SETTING, value=value)
741
739
742 @assert_repo_settings
740 @assert_repo_settings
743 def delete_repo_svn_pattern(self, id_):
741 def delete_repo_svn_pattern(self, id_):
744 ui = self.repo_settings.UiDbModel.get(id_)
742 ui = self.repo_settings.UiDbModel.get(id_)
745 if ui and ui.repository.repo_name == self.repo_settings.repo:
743 if ui and ui.repository.repo_name == self.repo_settings.repo:
746 # only delete if it's the same repo as initialized settings
744 # only delete if it's the same repo as initialized settings
747 self.repo_settings.delete_ui(id_)
745 self.repo_settings.delete_ui(id_)
748 else:
746 else:
749 # raise error as if we wouldn't find this option
747 # raise error as if we wouldn't find this option
750 self.repo_settings.delete_ui(-1)
748 self.repo_settings.delete_ui(-1)
751
749
752 def delete_global_svn_pattern(self, id_):
750 def delete_global_svn_pattern(self, id_):
753 self.global_settings.delete_ui(id_)
751 self.global_settings.delete_ui(id_)
754
752
755 @assert_repo_settings
753 @assert_repo_settings
756 def get_repo_ui_settings(self, section=None, key=None):
754 def get_repo_ui_settings(self, section=None, key=None):
757 global_uis = self.global_settings.get_ui(section, key)
755 global_uis = self.global_settings.get_ui(section, key)
758 repo_uis = self.repo_settings.get_ui(section, key)
756 repo_uis = self.repo_settings.get_ui(section, key)
759
757
760 filtered_repo_uis = self._filter_ui_settings(repo_uis)
758 filtered_repo_uis = self._filter_ui_settings(repo_uis)
761 filtered_repo_uis_keys = [
759 filtered_repo_uis_keys = [
762 (s.section, s.key) for s in filtered_repo_uis]
760 (s.section, s.key) for s in filtered_repo_uis]
763
761
764 def _is_global_ui_filtered(ui):
762 def _is_global_ui_filtered(ui):
765 return (
763 return (
766 (ui.section, ui.key) in filtered_repo_uis_keys
764 (ui.section, ui.key) in filtered_repo_uis_keys
767 or ui.section in self._svn_sections)
765 or ui.section in self._svn_sections)
768
766
769 filtered_global_uis = [
767 filtered_global_uis = [
770 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
768 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
771
769
772 return filtered_global_uis + filtered_repo_uis
770 return filtered_global_uis + filtered_repo_uis
773
771
774 def get_global_ui_settings(self, section=None, key=None):
772 def get_global_ui_settings(self, section=None, key=None):
775 return self.global_settings.get_ui(section, key)
773 return self.global_settings.get_ui(section, key)
776
774
777 def get_ui_settings_as_config_obj(self, section=None, key=None):
775 def get_ui_settings_as_config_obj(self, section=None, key=None):
778 config = base.Config()
776 config = base.Config()
779
777
780 ui_settings = self.get_ui_settings(section=section, key=key)
778 ui_settings = self.get_ui_settings(section=section, key=key)
781
779
782 for entry in ui_settings:
780 for entry in ui_settings:
783 config.set(entry.section, entry.key, entry.value)
781 config.set(entry.section, entry.key, entry.value)
784
782
785 return config
783 return config
786
784
787 def get_ui_settings(self, section=None, key=None):
785 def get_ui_settings(self, section=None, key=None):
788 if not self.repo_settings or self.inherit_global_settings:
786 if not self.repo_settings or self.inherit_global_settings:
789 return self.get_global_ui_settings(section, key)
787 return self.get_global_ui_settings(section, key)
790 else:
788 else:
791 return self.get_repo_ui_settings(section, key)
789 return self.get_repo_ui_settings(section, key)
792
790
793 def get_svn_patterns(self, section=None):
791 def get_svn_patterns(self, section=None):
794 if not self.repo_settings:
792 if not self.repo_settings:
795 return self.get_global_ui_settings(section)
793 return self.get_global_ui_settings(section)
796 else:
794 else:
797 return self.get_repo_ui_settings(section)
795 return self.get_repo_ui_settings(section)
798
796
799 @assert_repo_settings
797 @assert_repo_settings
800 def get_repo_general_settings(self):
798 def get_repo_general_settings(self):
801 global_settings = self.global_settings.get_all_settings()
799 global_settings = self.global_settings.get_all_settings()
802 repo_settings = self.repo_settings.get_all_settings()
800 repo_settings = self.repo_settings.get_all_settings()
803 filtered_repo_settings = self._filter_general_settings(repo_settings)
801 filtered_repo_settings = self._filter_general_settings(repo_settings)
804 global_settings.update(filtered_repo_settings)
802 global_settings.update(filtered_repo_settings)
805 return global_settings
803 return global_settings
806
804
807 def get_global_general_settings(self):
805 def get_global_general_settings(self):
808 return self.global_settings.get_all_settings()
806 return self.global_settings.get_all_settings()
809
807
810 def get_general_settings(self):
808 def get_general_settings(self):
811 if not self.repo_settings or self.inherit_global_settings:
809 if not self.repo_settings or self.inherit_global_settings:
812 return self.get_global_general_settings()
810 return self.get_global_general_settings()
813 else:
811 else:
814 return self.get_repo_general_settings()
812 return self.get_repo_general_settings()
815
813
816 def get_repos_location(self):
814 def get_repos_location(self):
817 return self.global_settings.get_ui_by_key('/').ui_value
815 return self.global_settings.get_ui_by_key('/').ui_value
818
816
819 def _filter_ui_settings(self, settings):
817 def _filter_ui_settings(self, settings):
820 filtered_settings = [
818 filtered_settings = [
821 s for s in settings if self._should_keep_setting(s)]
819 s for s in settings if self._should_keep_setting(s)]
822 return filtered_settings
820 return filtered_settings
823
821
824 def _should_keep_setting(self, setting):
822 def _should_keep_setting(self, setting):
825 keep = (
823 keep = (
826 (setting.section, setting.key) in self._ui_settings or
824 (setting.section, setting.key) in self._ui_settings or
827 setting.section in self._svn_sections)
825 setting.section in self._svn_sections)
828 return keep
826 return keep
829
827
830 def _filter_general_settings(self, settings):
828 def _filter_general_settings(self, settings):
831 keys = [self.get_keyname(key) for key in self.GENERAL_SETTINGS]
829 keys = [self.get_keyname(key) for key in self.GENERAL_SETTINGS]
832 return {
830 return {
833 k: settings[k]
831 k: settings[k]
834 for k in settings if k in keys}
832 for k in settings if k in keys}
835
833
836 def _collect_all_settings(self, global_=False):
834 def _collect_all_settings(self, global_=False):
837 settings = self.global_settings if global_ else self.repo_settings
835 settings = self.global_settings if global_ else self.repo_settings
838 result = {}
836 result = {}
839
837
840 for section, key in self._ui_settings:
838 for section, key in self._ui_settings:
841 ui = settings.get_ui_by_section_and_key(section, key)
839 ui = settings.get_ui_by_section_and_key(section, key)
842 result_key = self._get_form_ui_key(section, key)
840 result_key = self._get_form_ui_key(section, key)
843
841
844 if ui:
842 if ui:
845 if section in ('hooks', 'extensions'):
843 if section in ('hooks', 'extensions'):
846 result[result_key] = ui.ui_active
844 result[result_key] = ui.ui_active
847 elif result_key in ['vcs_git_lfs_enabled']:
845 elif result_key in ['vcs_git_lfs_enabled']:
848 result[result_key] = ui.ui_active
846 result[result_key] = ui.ui_active
849 else:
847 else:
850 result[result_key] = ui.ui_value
848 result[result_key] = ui.ui_value
851
849
852 for name in self.GENERAL_SETTINGS:
850 for name in self.GENERAL_SETTINGS:
853 setting = settings.get_setting_by_name(name)
851 setting = settings.get_setting_by_name(name)
854 if setting:
852 if setting:
855 result_key = self.get_keyname(name)
853 result_key = self.get_keyname(name)
856 result[result_key] = setting.app_settings_value
854 result[result_key] = setting.app_settings_value
857
855
858 return result
856 return result
859
857
860 def _get_form_ui_key(self, section, key):
858 def _get_form_ui_key(self, section, key):
861 return '{section}_{key}'.format(
859 return '{section}_{key}'.format(
862 section=section, key=key.replace('.', '_'))
860 section=section, key=key.replace('.', '_'))
863
861
864 def _create_or_update_ui(
862 def _create_or_update_ui(
865 self, settings, section, key, value=None, active=None):
863 self, settings, section, key, value=None, active=None):
866 ui = settings.get_ui_by_section_and_key(section, key)
864 ui = settings.get_ui_by_section_and_key(section, key)
867 if not ui:
865 if not ui:
868 active = True if active is None else active
866 active = True if active is None else active
869 settings.create_ui_section_value(
867 settings.create_ui_section_value(
870 section, value, key=key, active=active)
868 section, value, key=key, active=active)
871 else:
869 else:
872 if active is not None:
870 if active is not None:
873 ui.ui_active = active
871 ui.ui_active = active
874 if value is not None:
872 if value is not None:
875 ui.ui_value = value
873 ui.ui_value = value
876 Session().add(ui)
874 Session().add(ui)
877
875
878 def _create_svn_settings(self, settings, data):
876 def _create_svn_settings(self, settings, data):
879 svn_settings = {
877 svn_settings = {
880 'new_svn_branch': self.SVN_BRANCH_SECTION,
878 'new_svn_branch': self.SVN_BRANCH_SECTION,
881 'new_svn_tag': self.SVN_TAG_SECTION
879 'new_svn_tag': self.SVN_TAG_SECTION
882 }
880 }
883 for key in svn_settings:
881 for key in svn_settings:
884 if data.get(key):
882 if data.get(key):
885 settings.create_ui_section_value(svn_settings[key], data[key])
883 settings.create_ui_section_value(svn_settings[key], data[key])
886
884
887 def _create_or_update_general_settings(self, settings, data):
885 def _create_or_update_general_settings(self, settings, data):
888 for name in self.GENERAL_SETTINGS:
886 for name in self.GENERAL_SETTINGS:
889 data_key = self.get_keyname(name)
887 data_key = self.get_keyname(name)
890 if data_key not in data:
888 if data_key not in data:
891 raise ValueError(
889 raise ValueError(
892 f'The given data does not contain {data_key} key')
890 f'The given data does not contain {data_key} key')
893 setting = settings.create_or_update_setting(
891 setting = settings.create_or_update_setting(
894 name, data[data_key], 'bool')
892 name, data[data_key], 'bool')
895 Session().add(setting)
893 Session().add(setting)
896
894
897 def _get_settings_keys(self, settings, data):
895 def _get_settings_keys(self, settings, data):
898 data_keys = [self._get_form_ui_key(*s) for s in settings]
896 data_keys = [self._get_form_ui_key(*s) for s in settings]
899 for data_key in data_keys:
897 for data_key in data_keys:
900 if data_key not in data:
898 if data_key not in data:
901 raise ValueError(
899 raise ValueError(
902 f'The given data does not contain {data_key} key')
900 f'The given data does not contain {data_key} key')
903 return data_keys
901 return data_keys
904
902
905 def create_largeobjects_dirs_if_needed(self, repo_store_path):
903 def create_largeobjects_dirs_if_needed(self, repo_store_path):
906 """
904 """
907 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
905 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
908 does a repository scan if enabled in the settings.
906 does a repository scan if enabled in the settings.
909 """
907 """
910
908
911 from rhodecode.lib.vcs.backends.hg import largefiles_store
909 from rhodecode.lib.vcs.backends.hg import largefiles_store
912 from rhodecode.lib.vcs.backends.git import lfs_store
910 from rhodecode.lib.vcs.backends.git import lfs_store
913
911
914 paths = [
912 paths = [
915 largefiles_store(repo_store_path),
913 largefiles_store(repo_store_path),
916 lfs_store(repo_store_path)]
914 lfs_store(repo_store_path)]
917
915
918 for path in paths:
916 for path in paths:
919 if os.path.isdir(path):
917 if os.path.isdir(path):
920 continue
918 continue
921 if os.path.isfile(path):
919 if os.path.isfile(path):
922 continue
920 continue
923 # not a file nor dir, we try to create it
921 # not a file nor dir, we try to create it
924 try:
922 try:
925 os.makedirs(path)
923 os.makedirs(path)
926 except Exception:
924 except Exception:
927 log.warning('Failed to create largefiles dir:%s', path)
925 log.warning('Failed to create largefiles dir:%s', path)
@@ -1,384 +1,375 b''
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, allow_repo_location_change=False, **kwargs)">
6 <%def name="vcs_settings_fields(suffix='', svn_branch_patterns=None, svn_tag_patterns=None, repo_type=None, display_globals=False, allow_repo_location_change=False, **kwargs)">
7 % if display_globals:
7 % if display_globals:
8 <div class="panel panel-default">
8 <div class="panel panel-default">
9 <div class="panel-heading" id="general">
9 <div class="panel-heading" id="general">
10 <h3 class="panel-title">${_('General')}<a class="permalink" href="#general"> ΒΆ</a></h3>
10 <h3 class="panel-title">${_('General')}<a class="permalink" href="#general"> ΒΆ</a></h3>
11 </div>
11 </div>
12 <div class="panel-body">
12 <div class="panel-body">
13 <div class="field">
13 <div class="field">
14 <div class="checkbox">
14 <div class="checkbox">
15 ${h.checkbox('web_push_ssl' + suffix, 'True')}
15 ${h.checkbox('web_push_ssl' + suffix, 'True')}
16 <label for="web_push_ssl${suffix}">${_('Require SSL for vcs operations')}</label>
16 <label for="web_push_ssl${suffix}">${_('Require SSL for vcs operations')}</label>
17 </div>
17 </div>
18 <div class="label">
18 <div class="label">
19 <span class="help-block">${_('Activate to set RhodeCode to require SSL for pushing or pulling. If SSL certificate is missing it will return a HTTP Error 406: Not Acceptable.')}</span>
19 <span class="help-block">${_('Activate to set RhodeCode to require SSL for pushing or pulling. If SSL certificate is missing it will return a HTTP Error 406: Not Acceptable.')}</span>
20 </div>
20 </div>
21 </div>
21 </div>
22 </div>
22 </div>
23 </div>
23 </div>
24 % endif
24 % endif
25
25
26 % if display_globals:
26 % if display_globals:
27 <div class="panel panel-default">
27 <div class="panel panel-default">
28 <div class="panel-heading" id="vcs-storage-options">
28 <div class="panel-heading" id="vcs-storage-options">
29 <h3 class="panel-title">${_('Main Storage Location')}<a class="permalink" href="#vcs-storage-options"> ΒΆ</a></h3>
29 <h3 class="panel-title">${_('Main Storage Location')}<a class="permalink" href="#vcs-storage-options"> ΒΆ</a></h3>
30 </div>
30 </div>
31 <div class="panel-body">
31 <div class="panel-body">
32 <div class="field">
32 <div class="field">
33 <div class="inputx locked_input">
33 <div class="inputx locked_input">
34 %if allow_repo_location_change:
34 %if allow_repo_location_change:
35 ${h.text('paths_root_path',size=59,readonly="readonly", class_="disabled")}
35 ${h.text('paths_root_path',size=59,readonly="readonly", class_="disabled")}
36 <span id="path_unlock" class="tooltip"
36 <span id="path_unlock" class="tooltip"
37 title="${h.tooltip(_('Click to unlock. You must restart RhodeCode in order to make this setting take effect.'))}">
37 title="${h.tooltip(_('Click to unlock. You must restart RhodeCode in order to make this setting take effect.'))}">
38 <div class="btn btn-default lock_input_button"><i id="path_unlock_icon" class="icon-lock"></i></div>
38 <div class="btn btn-default lock_input_button"><i id="path_unlock_icon" class="icon-lock"></i></div>
39 </span>
39 </span>
40 %else:
40 %else:
41 ${_('Repository location change is disabled. You can enable this by changing the `allow_repo_location_change` inside .ini file.')}
41 ${_('Repository location change is disabled. You can enable this by changing the `allow_repo_location_change` inside .ini file.')}
42 ## form still requires this but we cannot internally change it anyway
42 ## form still requires this but we cannot internally change it anyway
43 ${h.hidden('paths_root_path',size=30,readonly="readonly", class_="disabled")}
43 ${h.hidden('paths_root_path',size=30,readonly="readonly", class_="disabled")}
44 %endif
44 %endif
45 </div>
45 </div>
46 </div>
46 </div>
47 <div class="label">
47 <div class="label">
48 <span class="help-block">${_('Filesystem location where repositories should be stored. After changing this value a restart and rescan of the repository folder are required.')}</span>
48 <span class="help-block">${_('Filesystem location where repositories should be stored. After changing this value a restart and rescan of the repository folder are required.')}</span>
49 </div>
49 </div>
50 </div>
50 </div>
51 </div>
51 </div>
52 % endif
52 % endif
53
53
54 % if display_globals or repo_type in ['git', 'hg']:
54 % if display_globals or repo_type in ['git', 'hg']:
55 <div class="panel panel-default">
55 <div class="panel panel-default">
56 <div class="panel-heading" id="vcs-hooks-options">
56 <div class="panel-heading" id="vcs-hooks-options">
57 <h3 class="panel-title">${_('Internal Hooks')}<a class="permalink" href="#vcs-hooks-options"> ΒΆ</a></h3>
57 <h3 class="panel-title">${_('Internal Hooks')}<a class="permalink" href="#vcs-hooks-options"> ΒΆ</a></h3>
58 </div>
58 </div>
59 <div class="panel-body">
59 <div class="panel-body">
60 <div class="field">
60 <div class="field">
61 <div class="checkbox">
61 <div class="checkbox">
62 ${h.checkbox('hooks_changegroup_repo_size' + suffix, 'True', **kwargs)}
62 ${h.checkbox('hooks_changegroup_repo_size' + suffix, 'True', **kwargs)}
63 <label for="hooks_changegroup_repo_size${suffix}">${_('Show repository size after push')}</label>
63 <label for="hooks_changegroup_repo_size${suffix}">${_('Show repository size after push')}</label>
64 </div>
64 </div>
65
65
66 <div class="label">
66 <div class="label">
67 <span class="help-block">${_('Trigger a hook that calculates repository size after each push.')}</span>
67 <span class="help-block">${_('Trigger a hook that calculates repository size after each push.')}</span>
68 </div>
68 </div>
69 <div class="checkbox">
69 <div class="checkbox">
70 ${h.checkbox('hooks_changegroup_push_logger' + suffix, 'True', **kwargs)}
70 ${h.checkbox('hooks_changegroup_push_logger' + suffix, 'True', **kwargs)}
71 <label for="hooks_changegroup_push_logger${suffix}">${_('Execute pre/post push hooks')}</label>
71 <label for="hooks_changegroup_push_logger${suffix}">${_('Execute pre/post push hooks')}</label>
72 </div>
72 </div>
73 <div class="label">
73 <div class="label">
74 <span class="help-block">${_('Execute Built in pre/post push hooks. This also executes rcextensions hooks.')}</span>
74 <span class="help-block">${_('Execute Built in pre/post push hooks. This also executes rcextensions hooks.')}</span>
75 </div>
75 </div>
76 <div class="checkbox">
76 <div class="checkbox">
77 ${h.checkbox('hooks_outgoing_pull_logger' + suffix, 'True', **kwargs)}
77 ${h.checkbox('hooks_outgoing_pull_logger' + suffix, 'True', **kwargs)}
78 <label for="hooks_outgoing_pull_logger${suffix}">${_('Execute pre/post pull hooks')}</label>
78 <label for="hooks_outgoing_pull_logger${suffix}">${_('Execute pre/post pull hooks')}</label>
79 </div>
79 </div>
80 <div class="label">
80 <div class="label">
81 <span class="help-block">${_('Execute Built in pre/post pull hooks. This also executes rcextensions hooks.')}</span>
81 <span class="help-block">${_('Execute Built in pre/post pull hooks. This also executes rcextensions hooks.')}</span>
82 </div>
82 </div>
83 </div>
83 </div>
84 </div>
84 </div>
85 </div>
85 </div>
86 % endif
86 % endif
87
87
88 % if display_globals or repo_type in ['hg']:
88 % if display_globals or repo_type in ['hg']:
89 <div class="panel panel-default">
89 <div class="panel panel-default">
90 <div class="panel-heading" id="vcs-hg-options">
90 <div class="panel-heading" id="vcs-hg-options">
91 <h3 class="panel-title">${_('Mercurial Settings')}<a class="permalink" href="#vcs-hg-options"> ΒΆ</a></h3>
91 <h3 class="panel-title">${_('Mercurial Settings')}<a class="permalink" href="#vcs-hg-options"> ΒΆ</a></h3>
92 </div>
92 </div>
93 <div class="panel-body">
93 <div class="panel-body">
94 <div class="checkbox">
94 <div class="checkbox">
95 ${h.checkbox('extensions_largefiles' + suffix, 'True', **kwargs)}
95 ${h.checkbox('extensions_largefiles' + suffix, 'True', **kwargs)}
96 <label for="extensions_largefiles${suffix}">${_('Enable largefiles extension')}</label>
96 <label for="extensions_largefiles${suffix}">${_('Enable largefiles extension')}</label>
97 </div>
97 </div>
98 <div class="label">
98 <div class="label">
99 % if display_globals:
99 % if display_globals:
100 <span class="help-block">${_('Enable Largefiles extensions for all repositories.')}</span>
100 <span class="help-block">${_('Enable Largefiles extensions for all repositories.')}</span>
101 % else:
101 % else:
102 <span class="help-block">${_('Enable Largefiles extensions for this repository.')}</span>
102 <span class="help-block">${_('Enable Largefiles extensions for this repository.')}</span>
103 % endif
103 % endif
104 </div>
104 </div>
105
105
106 % if display_globals:
106 % if display_globals:
107 <div class="field">
107 <div class="field">
108 <div class="input">
108 <div class="input">
109 ${h.text('largefiles_usercache' + suffix, size=59)}
109 ${h.text('largefiles_usercache' + suffix, size=59)}
110 </div>
110 </div>
111 </div>
111 </div>
112 <div class="label">
112 <div class="label">
113 <span class="help-block">${_('Filesystem location where Mercurial largefile objects should be stored.')}</span>
113 <span class="help-block">${_('Filesystem location where Mercurial largefile objects should be stored.')}</span>
114 </div>
114 </div>
115 % endif
115 % endif
116
116
117 <div class="checkbox">
117 <div class="checkbox">
118 ${h.checkbox('phases_publish' + suffix, 'True', **kwargs)}
118 ${h.checkbox('phases_publish' + suffix, 'True', **kwargs)}
119 <label for="phases_publish${suffix}">${_('Set repositories as publishing') if display_globals else _('Set repository as publishing')}</label>
119 <label for="phases_publish${suffix}">${_('Set repositories as publishing') if display_globals else _('Set repository as publishing')}</label>
120 </div>
120 </div>
121 <div class="label">
121 <div class="label">
122 <span class="help-block">${_('When this is enabled all commits in the repository are seen as public commits by clients.')}</span>
122 <span class="help-block">${_('When this is enabled all commits in the repository are seen as public commits by clients.')}</span>
123 </div>
123 </div>
124 % if display_globals:
125 <div class="checkbox">
126 ${h.checkbox('extensions_hgsubversion' + suffix,'True')}
127 <label for="extensions_hgsubversion${suffix}">${_('Enable hgsubversion extension')}</label>
128 </div>
129 <div class="label">
130 <span class="help-block">${_('Requires hgsubversion library to be installed. Allows cloning remote SVN repositories and migrates them to Mercurial type.')}</span>
131 </div>
132 % endif
133
124
134 <div class="checkbox">
125 <div class="checkbox">
135 ${h.checkbox('extensions_evolve' + suffix, 'True', **kwargs)}
126 ${h.checkbox('extensions_evolve' + suffix, 'True', **kwargs)}
136 <label for="extensions_evolve${suffix}">${_('Enable Evolve and Topic extension')}</label>
127 <label for="extensions_evolve${suffix}">${_('Enable Evolve and Topic extension')}</label>
137 </div>
128 </div>
138 <div class="label">
129 <div class="label">
139 % if display_globals:
130 % if display_globals:
140 <span class="help-block">${_('Enable Evolve and Topic extensions for all repositories.')}</span>
131 <span class="help-block">${_('Enable Evolve and Topic extensions for all repositories.')}</span>
141 % else:
132 % else:
142 <span class="help-block">${_('Enable Evolve and Topic extensions for this repository.')}</span>
133 <span class="help-block">${_('Enable Evolve and Topic extensions for this repository.')}</span>
143 % endif
134 % endif
144 </div>
135 </div>
145
136
146 </div>
137 </div>
147 </div>
138 </div>
148 % endif
139 % endif
149
140
150 % if display_globals or repo_type in ['git']:
141 % if display_globals or repo_type in ['git']:
151 <div class="panel panel-default">
142 <div class="panel panel-default">
152 <div class="panel-heading" id="vcs-git-options">
143 <div class="panel-heading" id="vcs-git-options">
153 <h3 class="panel-title">${_('Git Settings')}<a class="permalink" href="#vcs-git-options"> ΒΆ</a></h3>
144 <h3 class="panel-title">${_('Git Settings')}<a class="permalink" href="#vcs-git-options"> ΒΆ</a></h3>
154 </div>
145 </div>
155 <div class="panel-body">
146 <div class="panel-body">
156 <div class="checkbox">
147 <div class="checkbox">
157 ${h.checkbox('vcs_git_lfs_enabled' + suffix, 'True', **kwargs)}
148 ${h.checkbox('vcs_git_lfs_enabled' + suffix, 'True', **kwargs)}
158 <label for="vcs_git_lfs_enabled${suffix}">${_('Enable lfs extension')}</label>
149 <label for="vcs_git_lfs_enabled${suffix}">${_('Enable lfs extension')}</label>
159 </div>
150 </div>
160 <div class="label">
151 <div class="label">
161 % if display_globals:
152 % if display_globals:
162 <span class="help-block">${_('Enable lfs extensions for all repositories.')}</span>
153 <span class="help-block">${_('Enable lfs extensions for all repositories.')}</span>
163 % else:
154 % else:
164 <span class="help-block">${_('Enable lfs extensions for this repository.')}</span>
155 <span class="help-block">${_('Enable lfs extensions for this repository.')}</span>
165 % endif
156 % endif
166 </div>
157 </div>
167
158
168 % if display_globals:
159 % if display_globals:
169 <div class="field">
160 <div class="field">
170 <div class="input">
161 <div class="input">
171 ${h.text('vcs_git_lfs_store_location' + suffix, size=59)}
162 ${h.text('vcs_git_lfs_store_location' + suffix, size=59)}
172 </div>
163 </div>
173 </div>
164 </div>
174 <div class="label">
165 <div class="label">
175 <span class="help-block">${_('Filesystem location where Git lfs objects should be stored.')}</span>
166 <span class="help-block">${_('Filesystem location where Git lfs objects should be stored.')}</span>
176 </div>
167 </div>
177 % endif
168 % endif
178 </div>
169 </div>
179 </div>
170 </div>
180 % endif
171 % endif
181
172
182
173
183 % if display_globals:
174 % if display_globals:
184 <div class="panel panel-default">
175 <div class="panel panel-default">
185 <div class="panel-heading" id="vcs-global-svn-options">
176 <div class="panel-heading" id="vcs-global-svn-options">
186 <h3 class="panel-title">${_('Global Subversion Settings')}<a class="permalink" href="#vcs-global-svn-options"> ΒΆ</a></h3>
177 <h3 class="panel-title">${_('Global Subversion Settings')}<a class="permalink" href="#vcs-global-svn-options"> ΒΆ</a></h3>
187 </div>
178 </div>
188 <div class="panel-body">
179 <div class="panel-body">
189 <div class="field">
180 <div class="field">
190 <div class="checkbox">
181 <div class="checkbox">
191 ${h.checkbox('vcs_svn_proxy_http_requests_enabled' + suffix, 'True', **kwargs)}
182 ${h.checkbox('vcs_svn_proxy_http_requests_enabled' + suffix, 'True', **kwargs)}
192 <label for="vcs_svn_proxy_http_requests_enabled${suffix}">${_('Proxy subversion HTTP requests')}</label>
183 <label for="vcs_svn_proxy_http_requests_enabled${suffix}">${_('Proxy subversion HTTP requests')}</label>
193 </div>
184 </div>
194 <div class="label">
185 <div class="label">
195 <span class="help-block">
186 <span class="help-block">
196 ${_('Subversion HTTP Support. Enables communication with SVN over HTTP protocol.')}
187 ${_('Subversion HTTP Support. Enables communication with SVN over HTTP protocol.')}
197 <a href="${h.route_url('enterprise_svn_setup')}" target="_blank">${_('SVN Protocol setup Documentation')}</a>.
188 <a href="${h.route_url('enterprise_svn_setup')}" target="_blank">${_('SVN Protocol setup Documentation')}</a>.
198 </span>
189 </span>
199 </div>
190 </div>
200 </div>
191 </div>
201 <div class="field">
192 <div class="field">
202 <div class="label">
193 <div class="label">
203 <label for="vcs_svn_proxy_http_server_url">${_('Subversion HTTP Server URL')}</label><br/>
194 <label for="vcs_svn_proxy_http_server_url">${_('Subversion HTTP Server URL')}</label><br/>
204 </div>
195 </div>
205 <div class="input">
196 <div class="input">
206 ${h.text('vcs_svn_proxy_http_server_url',size=59)}
197 ${h.text('vcs_svn_proxy_http_server_url',size=59)}
207 % if c.svn_proxy_generate_config:
198 % if c.svn_proxy_generate_config:
208 <span class="buttons">
199 <span class="buttons">
209 <button class="btn btn-primary" id="vcs_svn_generate_cfg">${_('Generate Apache Config')}</button>
200 <button class="btn btn-primary" id="vcs_svn_generate_cfg">${_('Generate Apache Config')}</button>
210 </span>
201 </span>
211 % endif
202 % endif
212 </div>
203 </div>
213 </div>
204 </div>
214 </div>
205 </div>
215 </div>
206 </div>
216 % endif
207 % endif
217
208
218 % if display_globals or repo_type in ['svn']:
209 % if display_globals or repo_type in ['svn']:
219 <div class="panel panel-default">
210 <div class="panel panel-default">
220 <div class="panel-heading" id="vcs-svn-options">
211 <div class="panel-heading" id="vcs-svn-options">
221 <h3 class="panel-title">${_('Subversion Settings')}<a class="permalink" href="#vcs-svn-options"> ΒΆ</a></h3>
212 <h3 class="panel-title">${_('Subversion Settings')}<a class="permalink" href="#vcs-svn-options"> ΒΆ</a></h3>
222 </div>
213 </div>
223 <div class="panel-body">
214 <div class="panel-body">
224 <div class="field">
215 <div class="field">
225 <div class="content" >
216 <div class="content" >
226 <label>${_('Repository patterns')}</label><br/>
217 <label>${_('Repository patterns')}</label><br/>
227 </div>
218 </div>
228 </div>
219 </div>
229 <div class="label">
220 <div class="label">
230 <span class="help-block">${_('Patterns for identifying SVN branches and tags. For recursive search, use "*". Eg.: "/branches/*"')}</span>
221 <span class="help-block">${_('Patterns for identifying SVN branches and tags. For recursive search, use "*". Eg.: "/branches/*"')}</span>
231 </div>
222 </div>
232
223
233 <div class="field branch_patterns">
224 <div class="field branch_patterns">
234 <div class="input" >
225 <div class="input" >
235 <label>${_('Branches')}:</label><br/>
226 <label>${_('Branches')}:</label><br/>
236 </div>
227 </div>
237 % if svn_branch_patterns:
228 % if svn_branch_patterns:
238 % for branch in svn_branch_patterns:
229 % for branch in svn_branch_patterns:
239 <div class="input adjacent" id="${'id%s' % branch.ui_id}">
230 <div class="input adjacent" id="${'id%s' % branch.ui_id}">
240 ${h.hidden('branch_ui_key' + suffix, branch.ui_key)}
231 ${h.hidden('branch_ui_key' + suffix, branch.ui_key)}
241 ${h.text('branch_value_%d' % branch.ui_id + suffix, branch.ui_value, size=59, readonly="readonly", class_='disabled')}
232 ${h.text('branch_value_%d' % branch.ui_id + suffix, branch.ui_value, size=59, readonly="readonly", class_='disabled')}
242 % if kwargs.get('disabled') != 'disabled':
233 % if kwargs.get('disabled') != 'disabled':
243 <span class="btn btn-x" onclick="ajaxDeletePattern(${branch.ui_id},'${'id%s' % branch.ui_id}')">
234 <span class="btn btn-x" onclick="ajaxDeletePattern(${branch.ui_id},'${'id%s' % branch.ui_id}')">
244 ${_('Delete')}
235 ${_('Delete')}
245 </span>
236 </span>
246 % endif
237 % endif
247 </div>
238 </div>
248 % endfor
239 % endfor
249 %endif
240 %endif
250 </div>
241 </div>
251 % if kwargs.get('disabled') != 'disabled':
242 % if kwargs.get('disabled') != 'disabled':
252 <div class="field branch_patterns">
243 <div class="field branch_patterns">
253 <div class="input" >
244 <div class="input" >
254 ${h.text('new_svn_branch',size=59,placeholder='New branch pattern')}
245 ${h.text('new_svn_branch',size=59,placeholder='New branch pattern')}
255 </div>
246 </div>
256 </div>
247 </div>
257 % endif
248 % endif
258 <div class="field tag_patterns">
249 <div class="field tag_patterns">
259 <div class="input" >
250 <div class="input" >
260 <label>${_('Tags')}:</label><br/>
251 <label>${_('Tags')}:</label><br/>
261 </div>
252 </div>
262 % if svn_tag_patterns:
253 % if svn_tag_patterns:
263 % for tag in svn_tag_patterns:
254 % for tag in svn_tag_patterns:
264 <div class="input" id="${'id%s' % tag.ui_id + suffix}">
255 <div class="input" id="${'id%s' % tag.ui_id + suffix}">
265 ${h.hidden('tag_ui_key' + suffix, tag.ui_key)}
256 ${h.hidden('tag_ui_key' + suffix, tag.ui_key)}
266 ${h.text('tag_ui_value_new_%d' % tag.ui_id + suffix, tag.ui_value, size=59, readonly="readonly", class_='disabled tag_input')}
257 ${h.text('tag_ui_value_new_%d' % tag.ui_id + suffix, tag.ui_value, size=59, readonly="readonly", class_='disabled tag_input')}
267 % if kwargs.get('disabled') != 'disabled':
258 % if kwargs.get('disabled') != 'disabled':
268 <span class="btn btn-x" onclick="ajaxDeletePattern(${tag.ui_id},'${'id%s' % tag.ui_id}')">
259 <span class="btn btn-x" onclick="ajaxDeletePattern(${tag.ui_id},'${'id%s' % tag.ui_id}')">
269 ${_('Delete')}
260 ${_('Delete')}
270 </span>
261 </span>
271 %endif
262 %endif
272 </div>
263 </div>
273 % endfor
264 % endfor
274 % endif
265 % endif
275 </div>
266 </div>
276 % if kwargs.get('disabled') != 'disabled':
267 % if kwargs.get('disabled') != 'disabled':
277 <div class="field tag_patterns">
268 <div class="field tag_patterns">
278 <div class="input" >
269 <div class="input" >
279 ${h.text('new_svn_tag' + suffix, size=59, placeholder='New tag pattern')}
270 ${h.text('new_svn_tag' + suffix, size=59, placeholder='New tag pattern')}
280 </div>
271 </div>
281 </div>
272 </div>
282 %endif
273 %endif
283 </div>
274 </div>
284 </div>
275 </div>
285 % else:
276 % else:
286 ${h.hidden('new_svn_branch' + suffix, '')}
277 ${h.hidden('new_svn_branch' + suffix, '')}
287 ${h.hidden('new_svn_tag' + suffix, '')}
278 ${h.hidden('new_svn_tag' + suffix, '')}
288 % endif
279 % endif
289
280
290
281
291 % if display_globals or repo_type in ['hg', 'git']:
282 % if display_globals or repo_type in ['hg', 'git']:
292 <div class="panel panel-default">
283 <div class="panel panel-default">
293 <div class="panel-heading" id="vcs-pull-requests-options">
284 <div class="panel-heading" id="vcs-pull-requests-options">
294 <h3 class="panel-title">${_('Pull Request Settings')}<a class="permalink" href="#vcs-pull-requests-options"> ΒΆ</a></h3>
285 <h3 class="panel-title">${_('Pull Request Settings')}<a class="permalink" href="#vcs-pull-requests-options"> ΒΆ</a></h3>
295 </div>
286 </div>
296 <div class="panel-body">
287 <div class="panel-body">
297 <div class="checkbox">
288 <div class="checkbox">
298 ${h.checkbox('rhodecode_pr_merge_enabled' + suffix, 'True', **kwargs)}
289 ${h.checkbox('rhodecode_pr_merge_enabled' + suffix, 'True', **kwargs)}
299 <label for="rhodecode_pr_merge_enabled${suffix}">${_('Enable server-side merge for pull requests')}</label>
290 <label for="rhodecode_pr_merge_enabled${suffix}">${_('Enable server-side merge for pull requests')}</label>
300 </div>
291 </div>
301 <div class="label">
292 <div class="label">
302 <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>
293 <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>
303 </div>
294 </div>
304 <div class="checkbox">
295 <div class="checkbox">
305 ${h.checkbox('rhodecode_use_outdated_comments' + suffix, 'True', **kwargs)}
296 ${h.checkbox('rhodecode_use_outdated_comments' + suffix, 'True', **kwargs)}
306 <label for="rhodecode_use_outdated_comments${suffix}">${_('Invalidate and relocate inline comments during update')}</label>
297 <label for="rhodecode_use_outdated_comments${suffix}">${_('Invalidate and relocate inline comments during update')}</label>
307 </div>
298 </div>
308 <div class="label">
299 <div class="label">
309 <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>
300 <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>
310 </div>
301 </div>
311 </div>
302 </div>
312 </div>
303 </div>
313 % endif
304 % endif
314
305
315 % if display_globals or repo_type in ['hg', 'git', 'svn']:
306 % if display_globals or repo_type in ['hg', 'git', 'svn']:
316 <div class="panel panel-default">
307 <div class="panel panel-default">
317 <div class="panel-heading" id="vcs-pull-requests-options">
308 <div class="panel-heading" id="vcs-pull-requests-options">
318 <h3 class="panel-title">${_('Diff cache')}<a class="permalink" href="#vcs-pull-requests-options"> ΒΆ</a></h3>
309 <h3 class="panel-title">${_('Diff cache')}<a class="permalink" href="#vcs-pull-requests-options"> ΒΆ</a></h3>
319 </div>
310 </div>
320 <div class="panel-body">
311 <div class="panel-body">
321 <div class="checkbox">
312 <div class="checkbox">
322 ${h.checkbox('rhodecode_diff_cache' + suffix, 'True', **kwargs)}
313 ${h.checkbox('rhodecode_diff_cache' + suffix, 'True', **kwargs)}
323 <label for="rhodecode_diff_cache${suffix}">${_('Enable caching diffs for pull requests cache and commits')}</label>
314 <label for="rhodecode_diff_cache${suffix}">${_('Enable caching diffs for pull requests cache and commits')}</label>
324 </div>
315 </div>
325 </div>
316 </div>
326 </div>
317 </div>
327 % endif
318 % endif
328
319
329 % if display_globals or repo_type in ['hg',]:
320 % if display_globals or repo_type in ['hg',]:
330 <div class="panel panel-default">
321 <div class="panel panel-default">
331 <div class="panel-heading" id="vcs-pull-requests-options">
322 <div class="panel-heading" id="vcs-pull-requests-options">
332 <h3 class="panel-title">${_('Mercurial Pull Request Settings')}<a class="permalink" href="#vcs-hg-pull-requests-options"> ΒΆ</a></h3>
323 <h3 class="panel-title">${_('Mercurial Pull Request Settings')}<a class="permalink" href="#vcs-hg-pull-requests-options"> ΒΆ</a></h3>
333 </div>
324 </div>
334 <div class="panel-body">
325 <div class="panel-body">
335 ## Specific HG settings
326 ## Specific HG settings
336 <div class="checkbox">
327 <div class="checkbox">
337 ${h.checkbox('rhodecode_hg_use_rebase_for_merging' + suffix, 'True', **kwargs)}
328 ${h.checkbox('rhodecode_hg_use_rebase_for_merging' + suffix, 'True', **kwargs)}
338 <label for="rhodecode_hg_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
329 <label for="rhodecode_hg_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
339 </div>
330 </div>
340 <div class="label">
331 <div class="label">
341 <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
332 <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
342 </div>
333 </div>
343
334
344 <div class="checkbox">
335 <div class="checkbox">
345 ${h.checkbox('rhodecode_hg_close_branch_before_merging' + suffix, 'True', **kwargs)}
336 ${h.checkbox('rhodecode_hg_close_branch_before_merging' + suffix, 'True', **kwargs)}
346 <label for="rhodecode_hg_close_branch_before_merging{suffix}">${_('Close branch before merging it')}</label>
337 <label for="rhodecode_hg_close_branch_before_merging{suffix}">${_('Close branch before merging it')}</label>
347 </div>
338 </div>
348 <div class="label">
339 <div class="label">
349 <span class="help-block">${_('Close branch before merging it into destination branch. No effect when rebase strategy is use.')}</span>
340 <span class="help-block">${_('Close branch before merging it into destination branch. No effect when rebase strategy is use.')}</span>
350 </div>
341 </div>
351
342
352
343
353 </div>
344 </div>
354 </div>
345 </div>
355 % endif
346 % endif
356
347
357 ## DISABLED FOR GIT FOR NOW as the rebase/close is not supported yet
348 ## DISABLED FOR GIT FOR NOW as the rebase/close is not supported yet
358 ## % if display_globals or repo_type in ['git']:
349 ## % if display_globals or repo_type in ['git']:
359 ## <div class="panel panel-default">
350 ## <div class="panel panel-default">
360 ## <div class="panel-heading" id="vcs-pull-requests-options">
351 ## <div class="panel-heading" id="vcs-pull-requests-options">
361 ## <h3 class="panel-title">${_('Git Pull Request Settings')}<a class="permalink" href="#vcs-git-pull-requests-options"> ΒΆ</a></h3>
352 ## <h3 class="panel-title">${_('Git Pull Request Settings')}<a class="permalink" href="#vcs-git-pull-requests-options"> ΒΆ</a></h3>
362 ## </div>
353 ## </div>
363 ## <div class="panel-body">
354 ## <div class="panel-body">
364 ## <div class="checkbox">
355 ## <div class="checkbox">
365 ## ${h.checkbox('rhodecode_git_use_rebase_for_merging' + suffix, 'True', **kwargs)}
356 ## ${h.checkbox('rhodecode_git_use_rebase_for_merging' + suffix, 'True', **kwargs)}
366 ## <label for="rhodecode_git_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
357 ## <label for="rhodecode_git_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
367 ## </div>
358 ## </div>
368 ## <div class="label">
359 ## <div class="label">
369 ## <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
360 ## <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
370 ## </div>
361 ## </div>
371 ##
362 ##
372 ## <div class="checkbox">
363 ## <div class="checkbox">
373 ## ${h.checkbox('rhodecode_git_close_branch_before_merging' + suffix, 'True', **kwargs)}
364 ## ${h.checkbox('rhodecode_git_close_branch_before_merging' + suffix, 'True', **kwargs)}
374 ## <label for="rhodecode_git_close_branch_before_merging{suffix}">${_('Delete branch after merging it')}</label>
365 ## <label for="rhodecode_git_close_branch_before_merging{suffix}">${_('Delete branch after merging it')}</label>
375 ## </div>
366 ## </div>
376 ## <div class="label">
367 ## <div class="label">
377 ## <span class="help-block">${_('Delete branch after merging it into destination branch. No effect when rebase strategy is use.')}</span>
368 ## <span class="help-block">${_('Delete branch after merging it into destination branch. No effect when rebase strategy is use.')}</span>
378 ## </div>
369 ## </div>
379 ## </div>
370 ## </div>
380 ## </div>
371 ## </div>
381 ## % endif
372 ## % endif
382
373
383
374
384 </%def>
375 </%def>
@@ -1,1142 +1,1140 b''
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_use_outdated_comments': True,
41 'rhodecode_use_outdated_comments': True,
42 'rhodecode_hg_use_rebase_for_merging': True,
42 'rhodecode_hg_use_rebase_for_merging': True,
43 'rhodecode_hg_close_branch_before_merging': True,
43 'rhodecode_hg_close_branch_before_merging': True,
44 'rhodecode_git_use_rebase_for_merging': True,
44 'rhodecode_git_use_rebase_for_merging': True,
45 'rhodecode_git_close_branch_before_merging': True,
45 'rhodecode_git_close_branch_before_merging': True,
46 'rhodecode_diff_cache': True,
46 'rhodecode_diff_cache': True,
47 }
47 }
48
48
49
49
50 class TestInheritGlobalSettingsProperty(object):
50 class TestInheritGlobalSettingsProperty(object):
51 def test_get_raises_exception_when_repository_not_specified(self):
51 def test_get_raises_exception_when_repository_not_specified(self):
52 model = VcsSettingsModel()
52 model = VcsSettingsModel()
53 with pytest.raises(Exception) as exc_info:
53 with pytest.raises(Exception) as exc_info:
54 model.inherit_global_settings
54 model.inherit_global_settings
55 assert str(exc_info.value) == 'Repository is not specified'
55 assert str(exc_info.value) == 'Repository is not specified'
56
56
57 def test_true_is_returned_when_value_is_not_found(self, repo_stub):
57 def test_true_is_returned_when_value_is_not_found(self, repo_stub):
58 model = VcsSettingsModel(repo=repo_stub.repo_name)
58 model = VcsSettingsModel(repo=repo_stub.repo_name)
59 assert model.inherit_global_settings is True
59 assert model.inherit_global_settings is True
60
60
61 def test_value_is_returned(self, repo_stub, settings_util):
61 def test_value_is_returned(self, repo_stub, settings_util):
62 model = VcsSettingsModel(repo=repo_stub.repo_name)
62 model = VcsSettingsModel(repo=repo_stub.repo_name)
63 settings_util.create_repo_rhodecode_setting(
63 settings_util.create_repo_rhodecode_setting(
64 repo_stub, VcsSettingsModel.INHERIT_SETTINGS, False, 'bool')
64 repo_stub, VcsSettingsModel.INHERIT_SETTINGS, False, 'bool')
65 assert model.inherit_global_settings is False
65 assert model.inherit_global_settings is False
66
66
67 def test_value_is_set(self, repo_stub):
67 def test_value_is_set(self, repo_stub):
68 model = VcsSettingsModel(repo=repo_stub.repo_name)
68 model = VcsSettingsModel(repo=repo_stub.repo_name)
69 model.inherit_global_settings = False
69 model.inherit_global_settings = False
70 setting = model.repo_settings.get_setting_by_name(
70 setting = model.repo_settings.get_setting_by_name(
71 VcsSettingsModel.INHERIT_SETTINGS)
71 VcsSettingsModel.INHERIT_SETTINGS)
72 try:
72 try:
73 assert setting.app_settings_type == 'bool'
73 assert setting.app_settings_type == 'bool'
74 assert setting.app_settings_value is False
74 assert setting.app_settings_value is False
75 finally:
75 finally:
76 Session().delete(setting)
76 Session().delete(setting)
77 Session().commit()
77 Session().commit()
78
78
79 def test_set_raises_exception_when_repository_not_specified(self):
79 def test_set_raises_exception_when_repository_not_specified(self):
80 model = VcsSettingsModel()
80 model = VcsSettingsModel()
81 with pytest.raises(Exception) as exc_info:
81 with pytest.raises(Exception) as exc_info:
82 model.inherit_global_settings = False
82 model.inherit_global_settings = False
83 assert str(exc_info.value) == 'Repository is not specified'
83 assert str(exc_info.value) == 'Repository is not specified'
84
84
85
85
86 class TestVcsSettingsModel(object):
86 class TestVcsSettingsModel(object):
87 def test_global_svn_branch_patterns(self):
87 def test_global_svn_branch_patterns(self):
88 model = VcsSettingsModel()
88 model = VcsSettingsModel()
89 expected_result = {'test': 'test'}
89 expected_result = {'test': 'test'}
90 with mock.patch.object(model, 'global_settings') as settings_mock:
90 with mock.patch.object(model, 'global_settings') as settings_mock:
91 get_settings = settings_mock.get_ui_by_section
91 get_settings = settings_mock.get_ui_by_section
92 get_settings.return_value = expected_result
92 get_settings.return_value = expected_result
93 settings_mock.return_value = expected_result
93 settings_mock.return_value = expected_result
94 result = model.get_global_svn_branch_patterns()
94 result = model.get_global_svn_branch_patterns()
95
95
96 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
96 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
97 assert expected_result == result
97 assert expected_result == result
98
98
99 def test_repo_svn_branch_patterns(self):
99 def test_repo_svn_branch_patterns(self):
100 model = VcsSettingsModel()
100 model = VcsSettingsModel()
101 expected_result = {'test': 'test'}
101 expected_result = {'test': 'test'}
102 with mock.patch.object(model, 'repo_settings') as settings_mock:
102 with mock.patch.object(model, 'repo_settings') as settings_mock:
103 get_settings = settings_mock.get_ui_by_section
103 get_settings = settings_mock.get_ui_by_section
104 get_settings.return_value = expected_result
104 get_settings.return_value = expected_result
105 settings_mock.return_value = expected_result
105 settings_mock.return_value = expected_result
106 result = model.get_repo_svn_branch_patterns()
106 result = model.get_repo_svn_branch_patterns()
107
107
108 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
108 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
109 assert expected_result == result
109 assert expected_result == result
110
110
111 def test_repo_svn_branch_patterns_raises_exception_when_repo_is_not_set(
111 def test_repo_svn_branch_patterns_raises_exception_when_repo_is_not_set(
112 self):
112 self):
113 model = VcsSettingsModel()
113 model = VcsSettingsModel()
114 with pytest.raises(Exception) as exc_info:
114 with pytest.raises(Exception) as exc_info:
115 model.get_repo_svn_branch_patterns()
115 model.get_repo_svn_branch_patterns()
116 assert str(exc_info.value) == 'Repository is not specified'
116 assert str(exc_info.value) == 'Repository is not specified'
117
117
118 def test_global_svn_tag_patterns(self):
118 def test_global_svn_tag_patterns(self):
119 model = VcsSettingsModel()
119 model = VcsSettingsModel()
120 expected_result = {'test': 'test'}
120 expected_result = {'test': 'test'}
121 with mock.patch.object(model, 'global_settings') as settings_mock:
121 with mock.patch.object(model, 'global_settings') as settings_mock:
122 get_settings = settings_mock.get_ui_by_section
122 get_settings = settings_mock.get_ui_by_section
123 get_settings.return_value = expected_result
123 get_settings.return_value = expected_result
124 settings_mock.return_value = expected_result
124 settings_mock.return_value = expected_result
125 result = model.get_global_svn_tag_patterns()
125 result = model.get_global_svn_tag_patterns()
126
126
127 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
127 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
128 assert expected_result == result
128 assert expected_result == result
129
129
130 def test_repo_svn_tag_patterns(self):
130 def test_repo_svn_tag_patterns(self):
131 model = VcsSettingsModel()
131 model = VcsSettingsModel()
132 expected_result = {'test': 'test'}
132 expected_result = {'test': 'test'}
133 with mock.patch.object(model, 'repo_settings') as settings_mock:
133 with mock.patch.object(model, 'repo_settings') as settings_mock:
134 get_settings = settings_mock.get_ui_by_section
134 get_settings = settings_mock.get_ui_by_section
135 get_settings.return_value = expected_result
135 get_settings.return_value = expected_result
136 settings_mock.return_value = expected_result
136 settings_mock.return_value = expected_result
137 result = model.get_repo_svn_tag_patterns()
137 result = model.get_repo_svn_tag_patterns()
138
138
139 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
139 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
140 assert expected_result == result
140 assert expected_result == result
141
141
142 def test_repo_svn_tag_patterns_raises_exception_when_repo_is_not_set(self):
142 def test_repo_svn_tag_patterns_raises_exception_when_repo_is_not_set(self):
143 model = VcsSettingsModel()
143 model = VcsSettingsModel()
144 with pytest.raises(Exception) as exc_info:
144 with pytest.raises(Exception) as exc_info:
145 model.get_repo_svn_tag_patterns()
145 model.get_repo_svn_tag_patterns()
146 assert str(exc_info.value) == 'Repository is not specified'
146 assert str(exc_info.value) == 'Repository is not specified'
147
147
148 def test_get_global_settings(self):
148 def test_get_global_settings(self):
149 expected_result = {'test': 'test'}
149 expected_result = {'test': 'test'}
150 model = VcsSettingsModel()
150 model = VcsSettingsModel()
151 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
151 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
152 collect_mock.return_value = expected_result
152 collect_mock.return_value = expected_result
153 result = model.get_global_settings()
153 result = model.get_global_settings()
154
154
155 collect_mock.assert_called_once_with(global_=True)
155 collect_mock.assert_called_once_with(global_=True)
156 assert result == expected_result
156 assert result == expected_result
157
157
158 def test_get_repo_settings(self, repo_stub):
158 def test_get_repo_settings(self, repo_stub):
159 model = VcsSettingsModel(repo=repo_stub.repo_name)
159 model = VcsSettingsModel(repo=repo_stub.repo_name)
160 expected_result = {'test': 'test'}
160 expected_result = {'test': 'test'}
161 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
161 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
162 collect_mock.return_value = expected_result
162 collect_mock.return_value = expected_result
163 result = model.get_repo_settings()
163 result = model.get_repo_settings()
164
164
165 collect_mock.assert_called_once_with(global_=False)
165 collect_mock.assert_called_once_with(global_=False)
166 assert result == expected_result
166 assert result == expected_result
167
167
168 @pytest.mark.parametrize('settings, global_', [
168 @pytest.mark.parametrize('settings, global_', [
169 ('global_settings', True),
169 ('global_settings', True),
170 ('repo_settings', False)
170 ('repo_settings', False)
171 ])
171 ])
172 def test_collect_all_settings(self, settings, global_):
172 def test_collect_all_settings(self, settings, global_):
173 model = VcsSettingsModel()
173 model = VcsSettingsModel()
174 result_mock = self._mock_result()
174 result_mock = self._mock_result()
175
175
176 settings_patch = mock.patch.object(model, settings)
176 settings_patch = mock.patch.object(model, settings)
177 with settings_patch as settings_mock:
177 with settings_patch as settings_mock:
178 settings_mock.get_ui_by_section_and_key.return_value = result_mock
178 settings_mock.get_ui_by_section_and_key.return_value = result_mock
179 settings_mock.get_setting_by_name.return_value = result_mock
179 settings_mock.get_setting_by_name.return_value = result_mock
180 result = model._collect_all_settings(global_=global_)
180 result = model._collect_all_settings(global_=global_)
181
181
182 ui_settings = model.HG_SETTINGS + model.GIT_SETTINGS + model.HOOKS_SETTINGS
182 ui_settings = model.HG_SETTINGS + model.GIT_SETTINGS + model.HOOKS_SETTINGS
183 self._assert_get_settings_calls(
183 self._assert_get_settings_calls(
184 settings_mock, ui_settings, model.GENERAL_SETTINGS)
184 settings_mock, ui_settings, model.GENERAL_SETTINGS)
185 self._assert_collect_all_settings_result(
185 self._assert_collect_all_settings_result(
186 ui_settings, model.GENERAL_SETTINGS, result)
186 ui_settings, model.GENERAL_SETTINGS, result)
187
187
188 @pytest.mark.parametrize('settings, global_', [
188 @pytest.mark.parametrize('settings, global_', [
189 ('global_settings', True),
189 ('global_settings', True),
190 ('repo_settings', False)
190 ('repo_settings', False)
191 ])
191 ])
192 def test_collect_all_settings_without_empty_value(self, settings, global_):
192 def test_collect_all_settings_without_empty_value(self, settings, global_):
193 model = VcsSettingsModel()
193 model = VcsSettingsModel()
194
194
195 settings_patch = mock.patch.object(model, settings)
195 settings_patch = mock.patch.object(model, settings)
196 with settings_patch as settings_mock:
196 with settings_patch as settings_mock:
197 settings_mock.get_ui_by_section_and_key.return_value = None
197 settings_mock.get_ui_by_section_and_key.return_value = None
198 settings_mock.get_setting_by_name.return_value = None
198 settings_mock.get_setting_by_name.return_value = None
199 result = model._collect_all_settings(global_=global_)
199 result = model._collect_all_settings(global_=global_)
200
200
201 assert result == {}
201 assert result == {}
202
202
203 def _mock_result(self):
203 def _mock_result(self):
204 result_mock = mock.Mock()
204 result_mock = mock.Mock()
205 result_mock.ui_value = 'ui_value'
205 result_mock.ui_value = 'ui_value'
206 result_mock.ui_active = True
206 result_mock.ui_active = True
207 result_mock.app_settings_value = 'setting_value'
207 result_mock.app_settings_value = 'setting_value'
208 return result_mock
208 return result_mock
209
209
210 def _assert_get_settings_calls(
210 def _assert_get_settings_calls(
211 self, settings_mock, ui_settings, general_settings):
211 self, settings_mock, ui_settings, general_settings):
212 assert (
212 assert (
213 settings_mock.get_ui_by_section_and_key.call_count ==
213 settings_mock.get_ui_by_section_and_key.call_count ==
214 len(ui_settings))
214 len(ui_settings))
215 assert (
215 assert (
216 settings_mock.get_setting_by_name.call_count ==
216 settings_mock.get_setting_by_name.call_count ==
217 len(general_settings))
217 len(general_settings))
218
218
219 for section, key in ui_settings:
219 for section, key in ui_settings:
220 expected_call = mock.call(section, key)
220 expected_call = mock.call(section, key)
221 assert (
221 assert (
222 expected_call in
222 expected_call in
223 settings_mock.get_ui_by_section_and_key.call_args_list)
223 settings_mock.get_ui_by_section_and_key.call_args_list)
224
224
225 for name in general_settings:
225 for name in general_settings:
226 expected_call = mock.call(name)
226 expected_call = mock.call(name)
227 assert (
227 assert (
228 expected_call in
228 expected_call in
229 settings_mock.get_setting_by_name.call_args_list)
229 settings_mock.get_setting_by_name.call_args_list)
230
230
231 def _assert_collect_all_settings_result(
231 def _assert_collect_all_settings_result(
232 self, ui_settings, general_settings, result):
232 self, ui_settings, general_settings, result):
233 expected_result = {}
233 expected_result = {}
234 for section, key in ui_settings:
234 for section, key in ui_settings:
235 key = '{}_{}'.format(section, key.replace('.', '_'))
235 key = '{}_{}'.format(section, key.replace('.', '_'))
236
236
237 if section in ('extensions', 'hooks'):
237 if section in ('extensions', 'hooks'):
238 value = True
238 value = True
239 elif key in ['vcs_git_lfs_enabled']:
239 elif key in ['vcs_git_lfs_enabled']:
240 value = True
240 value = True
241 else:
241 else:
242 value = 'ui_value'
242 value = 'ui_value'
243 expected_result[key] = value
243 expected_result[key] = value
244
244
245 for name in general_settings:
245 for name in general_settings:
246 key = 'rhodecode_' + name
246 key = 'rhodecode_' + name
247 expected_result[key] = 'setting_value'
247 expected_result[key] = 'setting_value'
248
248
249 assert expected_result == result
249 assert expected_result == result
250
250
251
251
252 class TestCreateOrUpdateRepoHookSettings(object):
252 class TestCreateOrUpdateRepoHookSettings(object):
253 def test_create_when_no_repo_object_found(self, repo_stub):
253 def test_create_when_no_repo_object_found(self, repo_stub):
254 model = VcsSettingsModel(repo=repo_stub.repo_name)
254 model = VcsSettingsModel(repo=repo_stub.repo_name)
255
255
256 self._create_settings(model, HOOKS_FORM_DATA)
256 self._create_settings(model, HOOKS_FORM_DATA)
257
257
258 cleanup = []
258 cleanup = []
259 try:
259 try:
260 for section, key in model.HOOKS_SETTINGS:
260 for section, key in model.HOOKS_SETTINGS:
261 ui = model.repo_settings.get_ui_by_section_and_key(
261 ui = model.repo_settings.get_ui_by_section_and_key(
262 section, key)
262 section, key)
263 assert ui.ui_active is True
263 assert ui.ui_active is True
264 cleanup.append(ui)
264 cleanup.append(ui)
265 finally:
265 finally:
266 for ui in cleanup:
266 for ui in cleanup:
267 Session().delete(ui)
267 Session().delete(ui)
268 Session().commit()
268 Session().commit()
269
269
270 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
270 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
271 model = VcsSettingsModel(repo=repo_stub.repo_name)
271 model = VcsSettingsModel(repo=repo_stub.repo_name)
272
272
273 deleted_key = 'hooks_changegroup_repo_size'
273 deleted_key = 'hooks_changegroup_repo_size'
274 data = HOOKS_FORM_DATA.copy()
274 data = HOOKS_FORM_DATA.copy()
275 data.pop(deleted_key)
275 data.pop(deleted_key)
276
276
277 with pytest.raises(ValueError) as exc_info:
277 with pytest.raises(ValueError) as exc_info:
278 model.create_or_update_repo_hook_settings(data)
278 model.create_or_update_repo_hook_settings(data)
279 Session().commit()
279 Session().commit()
280
280
281 msg = 'The given data does not contain {} key'.format(deleted_key)
281 msg = 'The given data does not contain {} key'.format(deleted_key)
282 assert str(exc_info.value) == msg
282 assert str(exc_info.value) == msg
283
283
284 def test_update_when_repo_object_found(self, repo_stub, settings_util):
284 def test_update_when_repo_object_found(self, repo_stub, settings_util):
285 model = VcsSettingsModel(repo=repo_stub.repo_name)
285 model = VcsSettingsModel(repo=repo_stub.repo_name)
286 for section, key in model.HOOKS_SETTINGS:
286 for section, key in model.HOOKS_SETTINGS:
287 settings_util.create_repo_rhodecode_ui(
287 settings_util.create_repo_rhodecode_ui(
288 repo_stub, section, None, key=key, active=False)
288 repo_stub, section, None, key=key, active=False)
289 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
289 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
290 Session().commit()
290 Session().commit()
291
291
292 for section, key in model.HOOKS_SETTINGS:
292 for section, key in model.HOOKS_SETTINGS:
293 ui = model.repo_settings.get_ui_by_section_and_key(section, key)
293 ui = model.repo_settings.get_ui_by_section_and_key(section, key)
294 assert ui.ui_active is True
294 assert ui.ui_active is True
295
295
296 def _create_settings(self, model, data):
296 def _create_settings(self, model, data):
297 global_patch = mock.patch.object(model, 'global_settings')
297 global_patch = mock.patch.object(model, 'global_settings')
298 global_setting = mock.Mock()
298 global_setting = mock.Mock()
299 global_setting.ui_value = 'Test value'
299 global_setting.ui_value = 'Test value'
300 with global_patch as global_mock:
300 with global_patch as global_mock:
301 global_mock.get_ui_by_section_and_key.return_value = global_setting
301 global_mock.get_ui_by_section_and_key.return_value = global_setting
302 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
302 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
303 Session().commit()
303 Session().commit()
304
304
305
305
306 class TestUpdateGlobalHookSettings(object):
306 class TestUpdateGlobalHookSettings(object):
307 def test_update_raises_exception_when_data_incomplete(self):
307 def test_update_raises_exception_when_data_incomplete(self):
308 model = VcsSettingsModel()
308 model = VcsSettingsModel()
309
309
310 deleted_key = 'hooks_changegroup_repo_size'
310 deleted_key = 'hooks_changegroup_repo_size'
311 data = HOOKS_FORM_DATA.copy()
311 data = HOOKS_FORM_DATA.copy()
312 data.pop(deleted_key)
312 data.pop(deleted_key)
313
313
314 with pytest.raises(ValueError) as exc_info:
314 with pytest.raises(ValueError) as exc_info:
315 model.update_global_hook_settings(data)
315 model.update_global_hook_settings(data)
316 Session().commit()
316 Session().commit()
317
317
318 msg = 'The given data does not contain {} key'.format(deleted_key)
318 msg = 'The given data does not contain {} key'.format(deleted_key)
319 assert str(exc_info.value) == msg
319 assert str(exc_info.value) == msg
320
320
321 def test_update_global_hook_settings(self, settings_util):
321 def test_update_global_hook_settings(self, settings_util):
322 model = VcsSettingsModel()
322 model = VcsSettingsModel()
323 setting_mock = mock.MagicMock()
323 setting_mock = mock.MagicMock()
324 setting_mock.ui_active = False
324 setting_mock.ui_active = False
325 get_settings_patcher = mock.patch.object(
325 get_settings_patcher = mock.patch.object(
326 model.global_settings, 'get_ui_by_section_and_key',
326 model.global_settings, 'get_ui_by_section_and_key',
327 return_value=setting_mock)
327 return_value=setting_mock)
328 session_patcher = mock.patch('rhodecode.model.settings.Session')
328 session_patcher = mock.patch('rhodecode.model.settings.Session')
329 with get_settings_patcher as get_settings_mock, session_patcher:
329 with get_settings_patcher as get_settings_mock, session_patcher:
330 model.update_global_hook_settings(HOOKS_FORM_DATA)
330 model.update_global_hook_settings(HOOKS_FORM_DATA)
331 Session().commit()
331 Session().commit()
332
332
333 assert setting_mock.ui_active is True
333 assert setting_mock.ui_active is True
334 assert get_settings_mock.call_count == 3
334 assert get_settings_mock.call_count == 3
335
335
336
336
337 class TestCreateOrUpdateRepoGeneralSettings(object):
337 class TestCreateOrUpdateRepoGeneralSettings(object):
338 def test_calls_create_or_update_general_settings(self, repo_stub):
338 def test_calls_create_or_update_general_settings(self, repo_stub):
339 model = VcsSettingsModel(repo=repo_stub.repo_name)
339 model = VcsSettingsModel(repo=repo_stub.repo_name)
340 create_patch = mock.patch.object(
340 create_patch = mock.patch.object(
341 model, '_create_or_update_general_settings')
341 model, '_create_or_update_general_settings')
342 with create_patch as create_mock:
342 with create_patch as create_mock:
343 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
343 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
344 Session().commit()
344 Session().commit()
345
345
346 create_mock.assert_called_once_with(
346 create_mock.assert_called_once_with(
347 model.repo_settings, GENERAL_FORM_DATA)
347 model.repo_settings, GENERAL_FORM_DATA)
348
348
349 def test_raises_exception_when_repository_is_not_specified(self):
349 def test_raises_exception_when_repository_is_not_specified(self):
350 model = VcsSettingsModel()
350 model = VcsSettingsModel()
351 with pytest.raises(Exception) as exc_info:
351 with pytest.raises(Exception) as exc_info:
352 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
352 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
353 assert str(exc_info.value) == 'Repository is not specified'
353 assert str(exc_info.value) == 'Repository is not specified'
354
354
355
355
356 class TestCreateOrUpdatGlobalGeneralSettings(object):
356 class TestCreateOrUpdatGlobalGeneralSettings(object):
357 def test_calls_create_or_update_general_settings(self):
357 def test_calls_create_or_update_general_settings(self):
358 model = VcsSettingsModel()
358 model = VcsSettingsModel()
359 create_patch = mock.patch.object(
359 create_patch = mock.patch.object(
360 model, '_create_or_update_general_settings')
360 model, '_create_or_update_general_settings')
361 with create_patch as create_mock:
361 with create_patch as create_mock:
362 model.create_or_update_global_pr_settings(GENERAL_FORM_DATA)
362 model.create_or_update_global_pr_settings(GENERAL_FORM_DATA)
363 create_mock.assert_called_once_with(
363 create_mock.assert_called_once_with(
364 model.global_settings, GENERAL_FORM_DATA)
364 model.global_settings, GENERAL_FORM_DATA)
365
365
366
366
367 class TestCreateOrUpdateGeneralSettings(object):
367 class TestCreateOrUpdateGeneralSettings(object):
368 def test_create_when_no_repo_settings_found(self, repo_stub):
368 def test_create_when_no_repo_settings_found(self, repo_stub):
369 model = VcsSettingsModel(repo=repo_stub.repo_name)
369 model = VcsSettingsModel(repo=repo_stub.repo_name)
370 model._create_or_update_general_settings(
370 model._create_or_update_general_settings(
371 model.repo_settings, GENERAL_FORM_DATA)
371 model.repo_settings, GENERAL_FORM_DATA)
372
372
373 cleanup = []
373 cleanup = []
374 try:
374 try:
375 for name in model.GENERAL_SETTINGS:
375 for name in model.GENERAL_SETTINGS:
376 setting = model.repo_settings.get_setting_by_name(name)
376 setting = model.repo_settings.get_setting_by_name(name)
377 assert setting.app_settings_value is True
377 assert setting.app_settings_value is True
378 cleanup.append(setting)
378 cleanup.append(setting)
379 finally:
379 finally:
380 for setting in cleanup:
380 for setting in cleanup:
381 Session().delete(setting)
381 Session().delete(setting)
382 Session().commit()
382 Session().commit()
383
383
384 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
384 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
385 model = VcsSettingsModel(repo=repo_stub.repo_name)
385 model = VcsSettingsModel(repo=repo_stub.repo_name)
386
386
387 deleted_key = 'rhodecode_pr_merge_enabled'
387 deleted_key = 'rhodecode_pr_merge_enabled'
388 data = GENERAL_FORM_DATA.copy()
388 data = GENERAL_FORM_DATA.copy()
389 data.pop(deleted_key)
389 data.pop(deleted_key)
390
390
391 with pytest.raises(ValueError) as exc_info:
391 with pytest.raises(ValueError) as exc_info:
392 model._create_or_update_general_settings(model.repo_settings, data)
392 model._create_or_update_general_settings(model.repo_settings, data)
393 Session().commit()
393 Session().commit()
394
394
395 msg = 'The given data does not contain {} key'.format(deleted_key)
395 msg = 'The given data does not contain {} key'.format(deleted_key)
396 assert str(exc_info.value) == msg
396 assert str(exc_info.value) == msg
397
397
398 def test_update_when_repo_setting_found(self, repo_stub, settings_util):
398 def test_update_when_repo_setting_found(self, repo_stub, settings_util):
399 model = VcsSettingsModel(repo=repo_stub.repo_name)
399 model = VcsSettingsModel(repo=repo_stub.repo_name)
400 for name in model.GENERAL_SETTINGS:
400 for name in model.GENERAL_SETTINGS:
401 settings_util.create_repo_rhodecode_setting(
401 settings_util.create_repo_rhodecode_setting(
402 repo_stub, name, False, 'bool')
402 repo_stub, name, False, 'bool')
403
403
404 model._create_or_update_general_settings(
404 model._create_or_update_general_settings(
405 model.repo_settings, GENERAL_FORM_DATA)
405 model.repo_settings, GENERAL_FORM_DATA)
406 Session().commit()
406 Session().commit()
407
407
408 for name in model.GENERAL_SETTINGS:
408 for name in model.GENERAL_SETTINGS:
409 setting = model.repo_settings.get_setting_by_name(name)
409 setting = model.repo_settings.get_setting_by_name(name)
410 assert setting.app_settings_value is True
410 assert setting.app_settings_value is True
411
411
412
412
413 class TestCreateRepoSvnSettings(object):
413 class TestCreateRepoSvnSettings(object):
414 def test_calls_create_svn_settings(self, repo_stub):
414 def test_calls_create_svn_settings(self, repo_stub):
415 model = VcsSettingsModel(repo=repo_stub.repo_name)
415 model = VcsSettingsModel(repo=repo_stub.repo_name)
416 with mock.patch.object(model, '_create_svn_settings') as create_mock:
416 with mock.patch.object(model, '_create_svn_settings') as create_mock:
417 model.create_repo_svn_settings(SVN_FORM_DATA)
417 model.create_repo_svn_settings(SVN_FORM_DATA)
418 Session().commit()
418 Session().commit()
419
419
420 create_mock.assert_called_once_with(model.repo_settings, SVN_FORM_DATA)
420 create_mock.assert_called_once_with(model.repo_settings, SVN_FORM_DATA)
421
421
422 def test_raises_exception_when_repository_is_not_specified(self):
422 def test_raises_exception_when_repository_is_not_specified(self):
423 model = VcsSettingsModel()
423 model = VcsSettingsModel()
424 with pytest.raises(Exception) as exc_info:
424 with pytest.raises(Exception) as exc_info:
425 model.create_repo_svn_settings(SVN_FORM_DATA)
425 model.create_repo_svn_settings(SVN_FORM_DATA)
426 Session().commit()
426 Session().commit()
427
427
428 assert str(exc_info.value) == 'Repository is not specified'
428 assert str(exc_info.value) == 'Repository is not specified'
429
429
430
430
431 class TestCreateSvnSettings(object):
431 class TestCreateSvnSettings(object):
432 def test_create(self, repo_stub):
432 def test_create(self, repo_stub):
433 model = VcsSettingsModel(repo=repo_stub.repo_name)
433 model = VcsSettingsModel(repo=repo_stub.repo_name)
434 model._create_svn_settings(model.repo_settings, SVN_FORM_DATA)
434 model._create_svn_settings(model.repo_settings, SVN_FORM_DATA)
435 Session().commit()
435 Session().commit()
436
436
437 branch_ui = model.repo_settings.get_ui_by_section(
437 branch_ui = model.repo_settings.get_ui_by_section(
438 model.SVN_BRANCH_SECTION)
438 model.SVN_BRANCH_SECTION)
439 tag_ui = model.repo_settings.get_ui_by_section(
439 tag_ui = model.repo_settings.get_ui_by_section(
440 model.SVN_TAG_SECTION)
440 model.SVN_TAG_SECTION)
441
441
442 try:
442 try:
443 assert len(branch_ui) == 1
443 assert len(branch_ui) == 1
444 assert len(tag_ui) == 1
444 assert len(tag_ui) == 1
445 finally:
445 finally:
446 Session().delete(branch_ui[0])
446 Session().delete(branch_ui[0])
447 Session().delete(tag_ui[0])
447 Session().delete(tag_ui[0])
448 Session().commit()
448 Session().commit()
449
449
450 def test_create_tag(self, repo_stub):
450 def test_create_tag(self, repo_stub):
451 model = VcsSettingsModel(repo=repo_stub.repo_name)
451 model = VcsSettingsModel(repo=repo_stub.repo_name)
452 data = SVN_FORM_DATA.copy()
452 data = SVN_FORM_DATA.copy()
453 data.pop('new_svn_branch')
453 data.pop('new_svn_branch')
454 model._create_svn_settings(model.repo_settings, data)
454 model._create_svn_settings(model.repo_settings, data)
455 Session().commit()
455 Session().commit()
456
456
457 branch_ui = model.repo_settings.get_ui_by_section(
457 branch_ui = model.repo_settings.get_ui_by_section(
458 model.SVN_BRANCH_SECTION)
458 model.SVN_BRANCH_SECTION)
459 tag_ui = model.repo_settings.get_ui_by_section(
459 tag_ui = model.repo_settings.get_ui_by_section(
460 model.SVN_TAG_SECTION)
460 model.SVN_TAG_SECTION)
461
461
462 try:
462 try:
463 assert len(branch_ui) == 0
463 assert len(branch_ui) == 0
464 assert len(tag_ui) == 1
464 assert len(tag_ui) == 1
465 finally:
465 finally:
466 Session().delete(tag_ui[0])
466 Session().delete(tag_ui[0])
467 Session().commit()
467 Session().commit()
468
468
469 def test_create_nothing_when_no_svn_settings_specified(self, repo_stub):
469 def test_create_nothing_when_no_svn_settings_specified(self, repo_stub):
470 model = VcsSettingsModel(repo=repo_stub.repo_name)
470 model = VcsSettingsModel(repo=repo_stub.repo_name)
471 model._create_svn_settings(model.repo_settings, {})
471 model._create_svn_settings(model.repo_settings, {})
472 Session().commit()
472 Session().commit()
473
473
474 branch_ui = model.repo_settings.get_ui_by_section(
474 branch_ui = model.repo_settings.get_ui_by_section(
475 model.SVN_BRANCH_SECTION)
475 model.SVN_BRANCH_SECTION)
476 tag_ui = model.repo_settings.get_ui_by_section(
476 tag_ui = model.repo_settings.get_ui_by_section(
477 model.SVN_TAG_SECTION)
477 model.SVN_TAG_SECTION)
478
478
479 assert len(branch_ui) == 0
479 assert len(branch_ui) == 0
480 assert len(tag_ui) == 0
480 assert len(tag_ui) == 0
481
481
482 def test_create_nothing_when_empty_settings_specified(self, repo_stub):
482 def test_create_nothing_when_empty_settings_specified(self, repo_stub):
483 model = VcsSettingsModel(repo=repo_stub.repo_name)
483 model = VcsSettingsModel(repo=repo_stub.repo_name)
484 data = {
484 data = {
485 'new_svn_branch': '',
485 'new_svn_branch': '',
486 'new_svn_tag': ''
486 'new_svn_tag': ''
487 }
487 }
488 model._create_svn_settings(model.repo_settings, data)
488 model._create_svn_settings(model.repo_settings, data)
489 Session().commit()
489 Session().commit()
490
490
491 branch_ui = model.repo_settings.get_ui_by_section(
491 branch_ui = model.repo_settings.get_ui_by_section(
492 model.SVN_BRANCH_SECTION)
492 model.SVN_BRANCH_SECTION)
493 tag_ui = model.repo_settings.get_ui_by_section(
493 tag_ui = model.repo_settings.get_ui_by_section(
494 model.SVN_TAG_SECTION)
494 model.SVN_TAG_SECTION)
495
495
496 assert len(branch_ui) == 0
496 assert len(branch_ui) == 0
497 assert len(tag_ui) == 0
497 assert len(tag_ui) == 0
498
498
499
499
500 class TestCreateOrUpdateUi(object):
500 class TestCreateOrUpdateUi(object):
501 def test_create(self, repo_stub):
501 def test_create(self, repo_stub):
502 model = VcsSettingsModel(repo=repo_stub.repo_name)
502 model = VcsSettingsModel(repo=repo_stub.repo_name)
503 model._create_or_update_ui(
503 model._create_or_update_ui(
504 model.repo_settings, 'test-section', 'test-key', active=False,
504 model.repo_settings, 'test-section', 'test-key', active=False,
505 value='False')
505 value='False')
506 Session().commit()
506 Session().commit()
507
507
508 created_ui = model.repo_settings.get_ui_by_section_and_key(
508 created_ui = model.repo_settings.get_ui_by_section_and_key(
509 'test-section', 'test-key')
509 'test-section', 'test-key')
510
510
511 try:
511 try:
512 assert created_ui.ui_active is False
512 assert created_ui.ui_active is False
513 assert str2bool(created_ui.ui_value) is False
513 assert str2bool(created_ui.ui_value) is False
514 finally:
514 finally:
515 Session().delete(created_ui)
515 Session().delete(created_ui)
516 Session().commit()
516 Session().commit()
517
517
518 def test_update(self, repo_stub, settings_util):
518 def test_update(self, repo_stub, settings_util):
519 model = VcsSettingsModel(repo=repo_stub.repo_name)
519 model = VcsSettingsModel(repo=repo_stub.repo_name)
520 # care about only 3 first settings
520 # care about only 3 first settings
521 largefiles, phases, evolve = model.HG_SETTINGS[:3]
521 largefiles, phases, evolve = model.HG_SETTINGS[:3]
522
522
523 section = 'test-section'
523 section = 'test-section'
524 key = 'test-key'
524 key = 'test-key'
525 settings_util.create_repo_rhodecode_ui(
525 settings_util.create_repo_rhodecode_ui(
526 repo_stub, section, 'True', key=key, active=True)
526 repo_stub, section, 'True', key=key, active=True)
527
527
528 model._create_or_update_ui(
528 model._create_or_update_ui(
529 model.repo_settings, section, key, active=False, value='False')
529 model.repo_settings, section, key, active=False, value='False')
530 Session().commit()
530 Session().commit()
531
531
532 created_ui = model.repo_settings.get_ui_by_section_and_key(
532 created_ui = model.repo_settings.get_ui_by_section_and_key(
533 section, key)
533 section, key)
534 assert created_ui.ui_active is False
534 assert created_ui.ui_active is False
535 assert str2bool(created_ui.ui_value) is False
535 assert str2bool(created_ui.ui_value) is False
536
536
537
537
538 class TestCreateOrUpdateRepoHgSettings(object):
538 class TestCreateOrUpdateRepoHgSettings(object):
539 FORM_DATA = {
539 FORM_DATA = {
540 'extensions_largefiles': False,
540 'extensions_largefiles': False,
541 'extensions_evolve': False,
541 'extensions_evolve': False,
542 'phases_publish': False
542 'phases_publish': False
543 }
543 }
544
544
545 def test_creates_repo_hg_settings_when_data_is_correct(self, repo_stub):
545 def test_creates_repo_hg_settings_when_data_is_correct(self, repo_stub):
546 model = VcsSettingsModel(repo=repo_stub.repo_name)
546 model = VcsSettingsModel(repo=repo_stub.repo_name)
547 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
547 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
548 model.create_or_update_repo_hg_settings(self.FORM_DATA)
548 model.create_or_update_repo_hg_settings(self.FORM_DATA)
549 expected_calls = [
549 expected_calls = [
550 mock.call(model.repo_settings, 'extensions', 'largefiles', active=False, value=''),
550 mock.call(model.repo_settings, 'extensions', 'largefiles', active=False, value=''),
551 mock.call(model.repo_settings, 'extensions', 'evolve', active=False, value=''),
551 mock.call(model.repo_settings, 'extensions', 'evolve', active=False, value=''),
552 mock.call(model.repo_settings, 'experimental', 'evolution', active=False, value=''),
552 mock.call(model.repo_settings, 'experimental', 'evolution', active=False, value=''),
553 mock.call(model.repo_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
553 mock.call(model.repo_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
554 mock.call(model.repo_settings, 'extensions', 'topic', active=False, value=''),
554 mock.call(model.repo_settings, 'extensions', 'topic', active=False, value=''),
555 mock.call(model.repo_settings, 'phases', 'publish', value='False'),
555 mock.call(model.repo_settings, 'phases', 'publish', value='False'),
556 ]
556 ]
557 assert expected_calls == create_mock.call_args_list
557 assert expected_calls == create_mock.call_args_list
558
558
559 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
559 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
560 def test_key_is_not_found(self, repo_stub, field_to_remove):
560 def test_key_is_not_found(self, repo_stub, field_to_remove):
561 model = VcsSettingsModel(repo=repo_stub.repo_name)
561 model = VcsSettingsModel(repo=repo_stub.repo_name)
562 data = self.FORM_DATA.copy()
562 data = self.FORM_DATA.copy()
563 data.pop(field_to_remove)
563 data.pop(field_to_remove)
564 with pytest.raises(ValueError) as exc_info:
564 with pytest.raises(ValueError) as exc_info:
565 model.create_or_update_repo_hg_settings(data)
565 model.create_or_update_repo_hg_settings(data)
566 Session().commit()
566 Session().commit()
567
567
568 expected_message = 'The given data does not contain {} key'.format(
568 expected_message = 'The given data does not contain {} key'.format(
569 field_to_remove)
569 field_to_remove)
570 assert str(exc_info.value) == expected_message
570 assert str(exc_info.value) == expected_message
571
571
572 def test_create_raises_exception_when_repository_not_specified(self):
572 def test_create_raises_exception_when_repository_not_specified(self):
573 model = VcsSettingsModel()
573 model = VcsSettingsModel()
574 with pytest.raises(Exception) as exc_info:
574 with pytest.raises(Exception) as exc_info:
575 model.create_or_update_repo_hg_settings(self.FORM_DATA)
575 model.create_or_update_repo_hg_settings(self.FORM_DATA)
576 Session().commit()
576 Session().commit()
577
577
578 assert str(exc_info.value) == 'Repository is not specified'
578 assert str(exc_info.value) == 'Repository is not specified'
579
579
580
580
581 class TestUpdateGlobalSslSetting(object):
581 class TestUpdateGlobalSslSetting(object):
582 def test_updates_global_hg_settings(self):
582 def test_updates_global_hg_settings(self):
583 model = VcsSettingsModel()
583 model = VcsSettingsModel()
584 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
584 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
585 model.update_global_ssl_setting('False')
585 model.update_global_ssl_setting('False')
586 Session().commit()
586 Session().commit()
587
587
588 create_mock.assert_called_once_with(
588 create_mock.assert_called_once_with(
589 model.global_settings, 'web', 'push_ssl', value='False')
589 model.global_settings, 'web', 'push_ssl', value='False')
590
590
591
591
592 class TestUpdateGlobalPathSetting(object):
592 class TestUpdateGlobalPathSetting(object):
593 def test_updates_global_path_settings(self):
593 def test_updates_global_path_settings(self):
594 model = VcsSettingsModel()
594 model = VcsSettingsModel()
595 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
595 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
596 model.update_global_path_setting('False')
596 model.update_global_path_setting('False')
597 Session().commit()
597 Session().commit()
598
598
599 create_mock.assert_called_once_with(
599 create_mock.assert_called_once_with(
600 model.global_settings, 'paths', '/', value='False')
600 model.global_settings, 'paths', '/', value='False')
601
601
602
602
603 class TestCreateOrUpdateGlobalHgSettings(object):
603 class TestCreateOrUpdateGlobalHgSettings(object):
604 FORM_DATA = {
604 FORM_DATA = {
605 'extensions_largefiles': False,
605 'extensions_largefiles': False,
606 'largefiles_usercache': '/example/largefiles-store',
606 'largefiles_usercache': '/example/largefiles-store',
607 'phases_publish': False,
607 'phases_publish': False,
608 'extensions_hgsubversion': False,
609 'extensions_evolve': False
608 'extensions_evolve': False
610 }
609 }
611
610
612 def test_creates_repo_hg_settings_when_data_is_correct(self):
611 def test_creates_repo_hg_settings_when_data_is_correct(self):
613 model = VcsSettingsModel()
612 model = VcsSettingsModel()
614 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
613 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
615 model.create_or_update_global_hg_settings(self.FORM_DATA)
614 model.create_or_update_global_hg_settings(self.FORM_DATA)
616 Session().commit()
615 Session().commit()
617
616
618 expected_calls = [
617 expected_calls = [
619 mock.call(model.global_settings, 'extensions', 'largefiles', active=False, value=''),
618 mock.call(model.global_settings, 'extensions', 'largefiles', active=False, value=''),
620 mock.call(model.global_settings, 'largefiles', 'usercache', value='/example/largefiles-store'),
619 mock.call(model.global_settings, 'largefiles', 'usercache', value='/example/largefiles-store'),
621 mock.call(model.global_settings, 'phases', 'publish', value='False'),
620 mock.call(model.global_settings, 'phases', 'publish', value='False'),
622 mock.call(model.global_settings, 'extensions', 'hgsubversion', active=False),
623 mock.call(model.global_settings, 'extensions', 'evolve', active=False, value=''),
621 mock.call(model.global_settings, 'extensions', 'evolve', active=False, value=''),
624 mock.call(model.global_settings, 'experimental', 'evolution', active=False, value=''),
622 mock.call(model.global_settings, 'experimental', 'evolution', active=False, value=''),
625 mock.call(model.global_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
623 mock.call(model.global_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
626 mock.call(model.global_settings, 'extensions', 'topic', active=False, value=''),
624 mock.call(model.global_settings, 'extensions', 'topic', active=False, value=''),
627 ]
625 ]
628
626
629 assert expected_calls == create_mock.call_args_list
627 assert expected_calls == create_mock.call_args_list
630
628
631 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
629 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
632 def test_key_is_not_found(self, repo_stub, field_to_remove):
630 def test_key_is_not_found(self, repo_stub, field_to_remove):
633 model = VcsSettingsModel(repo=repo_stub.repo_name)
631 model = VcsSettingsModel(repo=repo_stub.repo_name)
634 data = self.FORM_DATA.copy()
632 data = self.FORM_DATA.copy()
635 data.pop(field_to_remove)
633 data.pop(field_to_remove)
636 with pytest.raises(Exception) as exc_info:
634 with pytest.raises(Exception) as exc_info:
637 model.create_or_update_global_hg_settings(data)
635 model.create_or_update_global_hg_settings(data)
638 Session().commit()
636 Session().commit()
639
637
640 expected_message = 'The given data does not contain {} key'.format(
638 expected_message = 'The given data does not contain {} key'.format(
641 field_to_remove)
639 field_to_remove)
642 assert str(exc_info.value) == expected_message
640 assert str(exc_info.value) == expected_message
643
641
644
642
645 class TestCreateOrUpdateGlobalGitSettings(object):
643 class TestCreateOrUpdateGlobalGitSettings(object):
646 FORM_DATA = {
644 FORM_DATA = {
647 'vcs_git_lfs_enabled': False,
645 'vcs_git_lfs_enabled': False,
648 'vcs_git_lfs_store_location': '/example/lfs-store',
646 'vcs_git_lfs_store_location': '/example/lfs-store',
649 }
647 }
650
648
651 def test_creates_repo_hg_settings_when_data_is_correct(self):
649 def test_creates_repo_hg_settings_when_data_is_correct(self):
652 model = VcsSettingsModel()
650 model = VcsSettingsModel()
653 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
651 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
654 model.create_or_update_global_git_settings(self.FORM_DATA)
652 model.create_or_update_global_git_settings(self.FORM_DATA)
655 Session().commit()
653 Session().commit()
656
654
657 expected_calls = [
655 expected_calls = [
658 mock.call(model.global_settings, 'vcs_git_lfs', 'enabled', active=False, value=False),
656 mock.call(model.global_settings, 'vcs_git_lfs', 'enabled', active=False, value=False),
659 mock.call(model.global_settings, 'vcs_git_lfs', 'store_location', value='/example/lfs-store'),
657 mock.call(model.global_settings, 'vcs_git_lfs', 'store_location', value='/example/lfs-store'),
660 ]
658 ]
661 assert expected_calls == create_mock.call_args_list
659 assert expected_calls == create_mock.call_args_list
662
660
663
661
664 class TestDeleteRepoSvnPattern(object):
662 class TestDeleteRepoSvnPattern(object):
665 def test_success_when_repo_is_set(self, backend_svn, settings_util):
663 def test_success_when_repo_is_set(self, backend_svn, settings_util):
666 repo = backend_svn.create_repo()
664 repo = backend_svn.create_repo()
667 repo_name = repo.repo_name
665 repo_name = repo.repo_name
668
666
669 model = VcsSettingsModel(repo=repo_name)
667 model = VcsSettingsModel(repo=repo_name)
670 entry = settings_util.create_repo_rhodecode_ui(
668 entry = settings_util.create_repo_rhodecode_ui(
671 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch')
669 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch')
672 Session().commit()
670 Session().commit()
673
671
674 model.delete_repo_svn_pattern(entry.ui_id)
672 model.delete_repo_svn_pattern(entry.ui_id)
675
673
676 def test_fail_when_delete_id_from_other_repo(self, backend_svn):
674 def test_fail_when_delete_id_from_other_repo(self, backend_svn):
677 repo_name = backend_svn.repo_name
675 repo_name = backend_svn.repo_name
678 model = VcsSettingsModel(repo=repo_name)
676 model = VcsSettingsModel(repo=repo_name)
679 delete_ui_patch = mock.patch.object(model.repo_settings, 'delete_ui')
677 delete_ui_patch = mock.patch.object(model.repo_settings, 'delete_ui')
680 with delete_ui_patch as delete_ui_mock:
678 with delete_ui_patch as delete_ui_mock:
681 model.delete_repo_svn_pattern(123)
679 model.delete_repo_svn_pattern(123)
682 Session().commit()
680 Session().commit()
683
681
684 delete_ui_mock.assert_called_once_with(-1)
682 delete_ui_mock.assert_called_once_with(-1)
685
683
686 def test_raises_exception_when_repository_is_not_specified(self):
684 def test_raises_exception_when_repository_is_not_specified(self):
687 model = VcsSettingsModel()
685 model = VcsSettingsModel()
688 with pytest.raises(Exception) as exc_info:
686 with pytest.raises(Exception) as exc_info:
689 model.delete_repo_svn_pattern(123)
687 model.delete_repo_svn_pattern(123)
690 assert str(exc_info.value) == 'Repository is not specified'
688 assert str(exc_info.value) == 'Repository is not specified'
691
689
692
690
693 class TestDeleteGlobalSvnPattern(object):
691 class TestDeleteGlobalSvnPattern(object):
694 def test_delete_global_svn_pattern_calls_delete_ui(self):
692 def test_delete_global_svn_pattern_calls_delete_ui(self):
695 model = VcsSettingsModel()
693 model = VcsSettingsModel()
696 delete_ui_patch = mock.patch.object(model.global_settings, 'delete_ui')
694 delete_ui_patch = mock.patch.object(model.global_settings, 'delete_ui')
697 with delete_ui_patch as delete_ui_mock:
695 with delete_ui_patch as delete_ui_mock:
698 model.delete_global_svn_pattern(123)
696 model.delete_global_svn_pattern(123)
699 delete_ui_mock.assert_called_once_with(123)
697 delete_ui_mock.assert_called_once_with(123)
700
698
701
699
702 class TestFilterUiSettings(object):
700 class TestFilterUiSettings(object):
703 def test_settings_are_filtered(self):
701 def test_settings_are_filtered(self):
704 model = VcsSettingsModel()
702 model = VcsSettingsModel()
705 repo_settings = [
703 repo_settings = [
706 UiSetting('extensions', 'largefiles', '', True),
704 UiSetting('extensions', 'largefiles', '', True),
707 UiSetting('phases', 'publish', 'True', True),
705 UiSetting('phases', 'publish', 'True', True),
708 UiSetting('hooks', 'changegroup.repo_size', 'hook', True),
706 UiSetting('hooks', 'changegroup.repo_size', 'hook', True),
709 UiSetting('hooks', 'changegroup.push_logger', 'hook', True),
707 UiSetting('hooks', 'changegroup.push_logger', 'hook', True),
710 UiSetting('hooks', 'outgoing.pull_logger', 'hook', True),
708 UiSetting('hooks', 'outgoing.pull_logger', 'hook', True),
711 UiSetting(
709 UiSetting(
712 'vcs_svn_branch', '84223c972204fa545ca1b22dac7bef5b68d7442d',
710 'vcs_svn_branch', '84223c972204fa545ca1b22dac7bef5b68d7442d',
713 'test_branch', True),
711 'test_branch', True),
714 UiSetting(
712 UiSetting(
715 'vcs_svn_tag', '84229c972204fa545ca1b22dac7bef5b68d7442d',
713 'vcs_svn_tag', '84229c972204fa545ca1b22dac7bef5b68d7442d',
716 'test_tag', True),
714 'test_tag', True),
717 ]
715 ]
718 non_repo_settings = [
716 non_repo_settings = [
719 UiSetting('largefiles', 'usercache', '/example/largefiles-store', True),
717 UiSetting('largefiles', 'usercache', '/example/largefiles-store', True),
720 UiSetting('test', 'outgoing.pull_logger', 'hook', True),
718 UiSetting('test', 'outgoing.pull_logger', 'hook', True),
721 UiSetting('hooks', 'test2', 'hook', True),
719 UiSetting('hooks', 'test2', 'hook', True),
722 UiSetting(
720 UiSetting(
723 'vcs_svn_repo', '84229c972204fa545ca1b22dac7bef5b68d7442d',
721 'vcs_svn_repo', '84229c972204fa545ca1b22dac7bef5b68d7442d',
724 'test_tag', True),
722 'test_tag', True),
725 ]
723 ]
726 settings = repo_settings + non_repo_settings
724 settings = repo_settings + non_repo_settings
727 filtered_settings = model._filter_ui_settings(settings)
725 filtered_settings = model._filter_ui_settings(settings)
728 assert sorted(filtered_settings) == sorted(repo_settings)
726 assert sorted(filtered_settings) == sorted(repo_settings)
729
727
730
728
731 class TestFilterGeneralSettings(object):
729 class TestFilterGeneralSettings(object):
732 def test_settings_are_filtered(self):
730 def test_settings_are_filtered(self):
733 model = VcsSettingsModel()
731 model = VcsSettingsModel()
734 settings = {
732 settings = {
735 'rhodecode_abcde': 'value1',
733 'rhodecode_abcde': 'value1',
736 'rhodecode_vwxyz': 'value2',
734 'rhodecode_vwxyz': 'value2',
737 }
735 }
738 general_settings = {
736 general_settings = {
739 'rhodecode_{}'.format(key): 'value'
737 'rhodecode_{}'.format(key): 'value'
740 for key in VcsSettingsModel.GENERAL_SETTINGS
738 for key in VcsSettingsModel.GENERAL_SETTINGS
741 }
739 }
742 settings.update(general_settings)
740 settings.update(general_settings)
743
741
744 filtered_settings = model._filter_general_settings(general_settings)
742 filtered_settings = model._filter_general_settings(general_settings)
745 assert sorted(filtered_settings) == sorted(general_settings)
743 assert sorted(filtered_settings) == sorted(general_settings)
746
744
747
745
748 class TestGetRepoUiSettings(object):
746 class TestGetRepoUiSettings(object):
749 def test_global_uis_are_returned_when_no_repo_uis_found(
747 def test_global_uis_are_returned_when_no_repo_uis_found(
750 self, repo_stub):
748 self, repo_stub):
751 model = VcsSettingsModel(repo=repo_stub.repo_name)
749 model = VcsSettingsModel(repo=repo_stub.repo_name)
752 result = model.get_repo_ui_settings()
750 result = model.get_repo_ui_settings()
753 svn_sections = (
751 svn_sections = (
754 VcsSettingsModel.SVN_TAG_SECTION,
752 VcsSettingsModel.SVN_TAG_SECTION,
755 VcsSettingsModel.SVN_BRANCH_SECTION)
753 VcsSettingsModel.SVN_BRANCH_SECTION)
756 expected_result = [
754 expected_result = [
757 s for s in model.global_settings.get_ui()
755 s for s in model.global_settings.get_ui()
758 if s.section not in svn_sections]
756 if s.section not in svn_sections]
759 assert sorted(result) == sorted(expected_result)
757 assert sorted(result) == sorted(expected_result)
760
758
761 def test_repo_uis_are_overriding_global_uis(
759 def test_repo_uis_are_overriding_global_uis(
762 self, repo_stub, settings_util):
760 self, repo_stub, settings_util):
763 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
761 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
764 settings_util.create_repo_rhodecode_ui(
762 settings_util.create_repo_rhodecode_ui(
765 repo_stub, section, 'repo', key=key, active=False)
763 repo_stub, section, 'repo', key=key, active=False)
766 model = VcsSettingsModel(repo=repo_stub.repo_name)
764 model = VcsSettingsModel(repo=repo_stub.repo_name)
767 result = model.get_repo_ui_settings()
765 result = model.get_repo_ui_settings()
768 for setting in result:
766 for setting in result:
769 locator = (setting.section, setting.key)
767 locator = (setting.section, setting.key)
770 if locator in VcsSettingsModel.HOOKS_SETTINGS:
768 if locator in VcsSettingsModel.HOOKS_SETTINGS:
771 assert setting.value == 'repo'
769 assert setting.value == 'repo'
772
770
773 assert setting.active is False
771 assert setting.active is False
774
772
775 def test_global_svn_patterns_are_not_in_list(
773 def test_global_svn_patterns_are_not_in_list(
776 self, repo_stub, settings_util):
774 self, repo_stub, settings_util):
777 svn_sections = (
775 svn_sections = (
778 VcsSettingsModel.SVN_TAG_SECTION,
776 VcsSettingsModel.SVN_TAG_SECTION,
779 VcsSettingsModel.SVN_BRANCH_SECTION)
777 VcsSettingsModel.SVN_BRANCH_SECTION)
780 for section in svn_sections:
778 for section in svn_sections:
781 settings_util.create_rhodecode_ui(
779 settings_util.create_rhodecode_ui(
782 section, 'repo', key='deadbeef' + section, active=False)
780 section, 'repo', key='deadbeef' + section, active=False)
783 Session().commit()
781 Session().commit()
784
782
785 model = VcsSettingsModel(repo=repo_stub.repo_name)
783 model = VcsSettingsModel(repo=repo_stub.repo_name)
786 result = model.get_repo_ui_settings()
784 result = model.get_repo_ui_settings()
787 for setting in result:
785 for setting in result:
788 assert setting.section not in svn_sections
786 assert setting.section not in svn_sections
789
787
790 def test_repo_uis_filtered_by_section_are_returned(
788 def test_repo_uis_filtered_by_section_are_returned(
791 self, repo_stub, settings_util):
789 self, repo_stub, settings_util):
792 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
790 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
793 settings_util.create_repo_rhodecode_ui(
791 settings_util.create_repo_rhodecode_ui(
794 repo_stub, section, 'repo', key=key, active=False)
792 repo_stub, section, 'repo', key=key, active=False)
795 model = VcsSettingsModel(repo=repo_stub.repo_name)
793 model = VcsSettingsModel(repo=repo_stub.repo_name)
796 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
794 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
797 result = model.get_repo_ui_settings(section=section)
795 result = model.get_repo_ui_settings(section=section)
798 for setting in result:
796 for setting in result:
799 assert setting.section == section
797 assert setting.section == section
800
798
801 def test_repo_uis_filtered_by_key_are_returned(
799 def test_repo_uis_filtered_by_key_are_returned(
802 self, repo_stub, settings_util):
800 self, repo_stub, settings_util):
803 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
801 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
804 settings_util.create_repo_rhodecode_ui(
802 settings_util.create_repo_rhodecode_ui(
805 repo_stub, section, 'repo', key=key, active=False)
803 repo_stub, section, 'repo', key=key, active=False)
806 model = VcsSettingsModel(repo=repo_stub.repo_name)
804 model = VcsSettingsModel(repo=repo_stub.repo_name)
807 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
805 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
808 result = model.get_repo_ui_settings(key=key)
806 result = model.get_repo_ui_settings(key=key)
809 for setting in result:
807 for setting in result:
810 assert setting.key == key
808 assert setting.key == key
811
809
812 def test_raises_exception_when_repository_is_not_specified(self):
810 def test_raises_exception_when_repository_is_not_specified(self):
813 model = VcsSettingsModel()
811 model = VcsSettingsModel()
814 with pytest.raises(Exception) as exc_info:
812 with pytest.raises(Exception) as exc_info:
815 model.get_repo_ui_settings()
813 model.get_repo_ui_settings()
816 assert str(exc_info.value) == 'Repository is not specified'
814 assert str(exc_info.value) == 'Repository is not specified'
817
815
818
816
819 class TestGetRepoGeneralSettings(object):
817 class TestGetRepoGeneralSettings(object):
820 def test_global_settings_are_returned_when_no_repo_settings_found(
818 def test_global_settings_are_returned_when_no_repo_settings_found(
821 self, repo_stub):
819 self, repo_stub):
822 model = VcsSettingsModel(repo=repo_stub.repo_name)
820 model = VcsSettingsModel(repo=repo_stub.repo_name)
823 result = model.get_repo_general_settings()
821 result = model.get_repo_general_settings()
824 expected_result = model.global_settings.get_all_settings()
822 expected_result = model.global_settings.get_all_settings()
825 assert sorted(result) == sorted(expected_result)
823 assert sorted(result) == sorted(expected_result)
826
824
827 def test_repo_uis_are_overriding_global_uis(
825 def test_repo_uis_are_overriding_global_uis(
828 self, repo_stub, settings_util):
826 self, repo_stub, settings_util):
829 for key in VcsSettingsModel.GENERAL_SETTINGS:
827 for key in VcsSettingsModel.GENERAL_SETTINGS:
830 settings_util.create_repo_rhodecode_setting(
828 settings_util.create_repo_rhodecode_setting(
831 repo_stub, key, 'abcde', type_='unicode')
829 repo_stub, key, 'abcde', type_='unicode')
832 Session().commit()
830 Session().commit()
833
831
834 model = VcsSettingsModel(repo=repo_stub.repo_name)
832 model = VcsSettingsModel(repo=repo_stub.repo_name)
835 result = model.get_repo_ui_settings()
833 result = model.get_repo_ui_settings()
836 for key in result:
834 for key in result:
837 if key in VcsSettingsModel.GENERAL_SETTINGS:
835 if key in VcsSettingsModel.GENERAL_SETTINGS:
838 assert result[key] == 'abcde'
836 assert result[key] == 'abcde'
839
837
840 def test_raises_exception_when_repository_is_not_specified(self):
838 def test_raises_exception_when_repository_is_not_specified(self):
841 model = VcsSettingsModel()
839 model = VcsSettingsModel()
842 with pytest.raises(Exception) as exc_info:
840 with pytest.raises(Exception) as exc_info:
843 model.get_repo_general_settings()
841 model.get_repo_general_settings()
844 assert str(exc_info.value) == 'Repository is not specified'
842 assert str(exc_info.value) == 'Repository is not specified'
845
843
846
844
847 class TestGetGlobalGeneralSettings(object):
845 class TestGetGlobalGeneralSettings(object):
848 def test_global_settings_are_returned(self, repo_stub):
846 def test_global_settings_are_returned(self, repo_stub):
849 model = VcsSettingsModel()
847 model = VcsSettingsModel()
850 result = model.get_global_general_settings()
848 result = model.get_global_general_settings()
851 expected_result = model.global_settings.get_all_settings()
849 expected_result = model.global_settings.get_all_settings()
852 assert sorted(result) == sorted(expected_result)
850 assert sorted(result) == sorted(expected_result)
853
851
854 def test_repo_uis_are_not_overriding_global_uis(
852 def test_repo_uis_are_not_overriding_global_uis(
855 self, repo_stub, settings_util):
853 self, repo_stub, settings_util):
856 for key in VcsSettingsModel.GENERAL_SETTINGS:
854 for key in VcsSettingsModel.GENERAL_SETTINGS:
857 settings_util.create_repo_rhodecode_setting(
855 settings_util.create_repo_rhodecode_setting(
858 repo_stub, key, 'abcde', type_='unicode')
856 repo_stub, key, 'abcde', type_='unicode')
859 Session().commit()
857 Session().commit()
860
858
861 model = VcsSettingsModel(repo=repo_stub.repo_name)
859 model = VcsSettingsModel(repo=repo_stub.repo_name)
862 result = model.get_global_general_settings()
860 result = model.get_global_general_settings()
863 expected_result = model.global_settings.get_all_settings()
861 expected_result = model.global_settings.get_all_settings()
864 assert sorted(result) == sorted(expected_result)
862 assert sorted(result) == sorted(expected_result)
865
863
866
864
867 class TestGetGlobalUiSettings(object):
865 class TestGetGlobalUiSettings(object):
868 def test_global_uis_are_returned(self, repo_stub):
866 def test_global_uis_are_returned(self, repo_stub):
869 model = VcsSettingsModel()
867 model = VcsSettingsModel()
870 result = model.get_global_ui_settings()
868 result = model.get_global_ui_settings()
871 expected_result = model.global_settings.get_ui()
869 expected_result = model.global_settings.get_ui()
872 assert sorted(result) == sorted(expected_result)
870 assert sorted(result) == sorted(expected_result)
873
871
874 def test_repo_uis_are_not_overriding_global_uis(
872 def test_repo_uis_are_not_overriding_global_uis(
875 self, repo_stub, settings_util):
873 self, repo_stub, settings_util):
876 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
874 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
877 settings_util.create_repo_rhodecode_ui(
875 settings_util.create_repo_rhodecode_ui(
878 repo_stub, section, 'repo', key=key, active=False)
876 repo_stub, section, 'repo', key=key, active=False)
879 Session().commit()
877 Session().commit()
880
878
881 model = VcsSettingsModel(repo=repo_stub.repo_name)
879 model = VcsSettingsModel(repo=repo_stub.repo_name)
882 result = model.get_global_ui_settings()
880 result = model.get_global_ui_settings()
883 expected_result = model.global_settings.get_ui()
881 expected_result = model.global_settings.get_ui()
884 assert sorted(result) == sorted(expected_result)
882 assert sorted(result) == sorted(expected_result)
885
883
886 def test_ui_settings_filtered_by_section(
884 def test_ui_settings_filtered_by_section(
887 self, repo_stub, settings_util):
885 self, repo_stub, settings_util):
888 model = VcsSettingsModel(repo=repo_stub.repo_name)
886 model = VcsSettingsModel(repo=repo_stub.repo_name)
889 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
887 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
890 result = model.get_global_ui_settings(section=section)
888 result = model.get_global_ui_settings(section=section)
891 expected_result = model.global_settings.get_ui(section=section)
889 expected_result = model.global_settings.get_ui(section=section)
892 assert sorted(result) == sorted(expected_result)
890 assert sorted(result) == sorted(expected_result)
893
891
894 def test_ui_settings_filtered_by_key(
892 def test_ui_settings_filtered_by_key(
895 self, repo_stub, settings_util):
893 self, repo_stub, settings_util):
896 model = VcsSettingsModel(repo=repo_stub.repo_name)
894 model = VcsSettingsModel(repo=repo_stub.repo_name)
897 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
895 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
898 result = model.get_global_ui_settings(key=key)
896 result = model.get_global_ui_settings(key=key)
899 expected_result = model.global_settings.get_ui(key=key)
897 expected_result = model.global_settings.get_ui(key=key)
900 assert sorted(result) == sorted(expected_result)
898 assert sorted(result) == sorted(expected_result)
901
899
902
900
903 class TestGetGeneralSettings(object):
901 class TestGetGeneralSettings(object):
904 def test_global_settings_are_returned_when_inherited_is_true(
902 def test_global_settings_are_returned_when_inherited_is_true(
905 self, repo_stub, settings_util):
903 self, repo_stub, settings_util):
906 model = VcsSettingsModel(repo=repo_stub.repo_name)
904 model = VcsSettingsModel(repo=repo_stub.repo_name)
907 model.inherit_global_settings = True
905 model.inherit_global_settings = True
908 for key in VcsSettingsModel.GENERAL_SETTINGS:
906 for key in VcsSettingsModel.GENERAL_SETTINGS:
909 settings_util.create_repo_rhodecode_setting(
907 settings_util.create_repo_rhodecode_setting(
910 repo_stub, key, 'abcde', type_='unicode')
908 repo_stub, key, 'abcde', type_='unicode')
911 Session().commit()
909 Session().commit()
912
910
913 result = model.get_general_settings()
911 result = model.get_general_settings()
914 expected_result = model.get_global_general_settings()
912 expected_result = model.get_global_general_settings()
915 assert sorted(result) == sorted(expected_result)
913 assert sorted(result) == sorted(expected_result)
916
914
917 def test_repo_settings_are_returned_when_inherited_is_false(
915 def test_repo_settings_are_returned_when_inherited_is_false(
918 self, repo_stub, settings_util):
916 self, repo_stub, settings_util):
919 model = VcsSettingsModel(repo=repo_stub.repo_name)
917 model = VcsSettingsModel(repo=repo_stub.repo_name)
920 model.inherit_global_settings = False
918 model.inherit_global_settings = False
921 for key in VcsSettingsModel.GENERAL_SETTINGS:
919 for key in VcsSettingsModel.GENERAL_SETTINGS:
922 settings_util.create_repo_rhodecode_setting(
920 settings_util.create_repo_rhodecode_setting(
923 repo_stub, key, 'abcde', type_='unicode')
921 repo_stub, key, 'abcde', type_='unicode')
924 Session().commit()
922 Session().commit()
925
923
926 result = model.get_general_settings()
924 result = model.get_general_settings()
927 expected_result = model.get_repo_general_settings()
925 expected_result = model.get_repo_general_settings()
928 assert sorted(result) == sorted(expected_result)
926 assert sorted(result) == sorted(expected_result)
929
927
930 def test_global_settings_are_returned_when_no_repository_specified(self):
928 def test_global_settings_are_returned_when_no_repository_specified(self):
931 model = VcsSettingsModel()
929 model = VcsSettingsModel()
932 result = model.get_general_settings()
930 result = model.get_general_settings()
933 expected_result = model.get_global_general_settings()
931 expected_result = model.get_global_general_settings()
934 assert sorted(result) == sorted(expected_result)
932 assert sorted(result) == sorted(expected_result)
935
933
936
934
937 class TestGetUiSettings(object):
935 class TestGetUiSettings(object):
938 def test_global_settings_are_returned_when_inherited_is_true(
936 def test_global_settings_are_returned_when_inherited_is_true(
939 self, repo_stub, settings_util):
937 self, repo_stub, settings_util):
940 model = VcsSettingsModel(repo=repo_stub.repo_name)
938 model = VcsSettingsModel(repo=repo_stub.repo_name)
941 model.inherit_global_settings = True
939 model.inherit_global_settings = True
942 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
940 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
943 settings_util.create_repo_rhodecode_ui(
941 settings_util.create_repo_rhodecode_ui(
944 repo_stub, section, 'repo', key=key, active=True)
942 repo_stub, section, 'repo', key=key, active=True)
945 Session().commit()
943 Session().commit()
946
944
947 result = model.get_ui_settings()
945 result = model.get_ui_settings()
948 expected_result = model.get_global_ui_settings()
946 expected_result = model.get_global_ui_settings()
949 assert sorted(result) == sorted(expected_result)
947 assert sorted(result) == sorted(expected_result)
950
948
951 def test_repo_settings_are_returned_when_inherited_is_false(
949 def test_repo_settings_are_returned_when_inherited_is_false(
952 self, repo_stub, settings_util):
950 self, repo_stub, settings_util):
953 model = VcsSettingsModel(repo=repo_stub.repo_name)
951 model = VcsSettingsModel(repo=repo_stub.repo_name)
954 model.inherit_global_settings = False
952 model.inherit_global_settings = False
955 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
953 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
956 settings_util.create_repo_rhodecode_ui(
954 settings_util.create_repo_rhodecode_ui(
957 repo_stub, section, 'repo', key=key, active=True)
955 repo_stub, section, 'repo', key=key, active=True)
958 Session().commit()
956 Session().commit()
959
957
960 result = model.get_ui_settings()
958 result = model.get_ui_settings()
961 expected_result = model.get_repo_ui_settings()
959 expected_result = model.get_repo_ui_settings()
962 assert sorted(result) == sorted(expected_result)
960 assert sorted(result) == sorted(expected_result)
963
961
964 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
962 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
965 model = VcsSettingsModel(repo=repo_stub.repo_name)
963 model = VcsSettingsModel(repo=repo_stub.repo_name)
966 model.inherit_global_settings = False
964 model.inherit_global_settings = False
967
965
968 args = ('section', 'key')
966 args = ('section', 'key')
969 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:
970 model.get_ui_settings(*args)
968 model.get_ui_settings(*args)
971 Session().commit()
969 Session().commit()
972
970
973 settings_mock.assert_called_once_with(*args)
971 settings_mock.assert_called_once_with(*args)
974
972
975 def test_global_settings_filtered_by_section_and_key(self):
973 def test_global_settings_filtered_by_section_and_key(self):
976 model = VcsSettingsModel()
974 model = VcsSettingsModel()
977 args = ('section', 'key')
975 args = ('section', 'key')
978 with mock.patch.object(model, 'get_global_ui_settings') as (
976 with mock.patch.object(model, 'get_global_ui_settings') as (
979 settings_mock):
977 settings_mock):
980 model.get_ui_settings(*args)
978 model.get_ui_settings(*args)
981 settings_mock.assert_called_once_with(*args)
979 settings_mock.assert_called_once_with(*args)
982
980
983 def test_global_settings_are_returned_when_no_repository_specified(self):
981 def test_global_settings_are_returned_when_no_repository_specified(self):
984 model = VcsSettingsModel()
982 model = VcsSettingsModel()
985 result = model.get_ui_settings()
983 result = model.get_ui_settings()
986 expected_result = model.get_global_ui_settings()
984 expected_result = model.get_global_ui_settings()
987 assert sorted(result) == sorted(expected_result)
985 assert sorted(result) == sorted(expected_result)
988
986
989
987
990 class TestGetSvnPatterns(object):
988 class TestGetSvnPatterns(object):
991 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
989 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
992 model = VcsSettingsModel(repo=repo_stub.repo_name)
990 model = VcsSettingsModel(repo=repo_stub.repo_name)
993 args = ('section', )
991 args = ('section', )
994 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
992 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
995 model.get_svn_patterns(*args)
993 model.get_svn_patterns(*args)
996
994
997 Session().commit()
995 Session().commit()
998 settings_mock.assert_called_once_with(*args)
996 settings_mock.assert_called_once_with(*args)
999
997
1000 def test_global_settings_filtered_by_section_and_key(self):
998 def test_global_settings_filtered_by_section_and_key(self):
1001 model = VcsSettingsModel()
999 model = VcsSettingsModel()
1002 args = ('section', )
1000 args = ('section', )
1003 with mock.patch.object(model, 'get_global_ui_settings') as (
1001 with mock.patch.object(model, 'get_global_ui_settings') as (
1004 settings_mock):
1002 settings_mock):
1005 model.get_svn_patterns(*args)
1003 model.get_svn_patterns(*args)
1006 settings_mock.assert_called_once_with(*args)
1004 settings_mock.assert_called_once_with(*args)
1007
1005
1008
1006
1009 class TestGetReposLocation(object):
1007 class TestGetReposLocation(object):
1010 def test_returns_repos_location(self, repo_stub):
1008 def test_returns_repos_location(self, repo_stub):
1011 model = VcsSettingsModel()
1009 model = VcsSettingsModel()
1012
1010
1013 result_mock = mock.Mock()
1011 result_mock = mock.Mock()
1014 result_mock.ui_value = '/tmp'
1012 result_mock.ui_value = '/tmp'
1015
1013
1016 with mock.patch.object(model, 'global_settings') as settings_mock:
1014 with mock.patch.object(model, 'global_settings') as settings_mock:
1017 settings_mock.get_ui_by_key.return_value = result_mock
1015 settings_mock.get_ui_by_key.return_value = result_mock
1018 result = model.get_repos_location()
1016 result = model.get_repos_location()
1019
1017
1020 settings_mock.get_ui_by_key.assert_called_once_with('/')
1018 settings_mock.get_ui_by_key.assert_called_once_with('/')
1021 assert result == '/tmp'
1019 assert result == '/tmp'
1022
1020
1023
1021
1024 class TestCreateOrUpdateRepoSettings(object):
1022 class TestCreateOrUpdateRepoSettings(object):
1025 FORM_DATA = {
1023 FORM_DATA = {
1026 'inherit_global_settings': False,
1024 'inherit_global_settings': False,
1027 'hooks_changegroup_repo_size': False,
1025 'hooks_changegroup_repo_size': False,
1028 'hooks_changegroup_push_logger': False,
1026 'hooks_changegroup_push_logger': False,
1029 'hooks_outgoing_pull_logger': False,
1027 'hooks_outgoing_pull_logger': False,
1030 'extensions_largefiles': False,
1028 'extensions_largefiles': False,
1031 'extensions_evolve': False,
1029 'extensions_evolve': False,
1032 'largefiles_usercache': '/example/largefiles-store',
1030 'largefiles_usercache': '/example/largefiles-store',
1033 'vcs_git_lfs_enabled': False,
1031 'vcs_git_lfs_enabled': False,
1034 'vcs_git_lfs_store_location': '/',
1032 'vcs_git_lfs_store_location': '/',
1035 'phases_publish': 'False',
1033 'phases_publish': 'False',
1036 'rhodecode_pr_merge_enabled': False,
1034 'rhodecode_pr_merge_enabled': False,
1037 'rhodecode_use_outdated_comments': False,
1035 'rhodecode_use_outdated_comments': False,
1038 'new_svn_branch': '',
1036 'new_svn_branch': '',
1039 'new_svn_tag': ''
1037 'new_svn_tag': ''
1040 }
1038 }
1041
1039
1042 def test_get_raises_exception_when_repository_not_specified(self):
1040 def test_get_raises_exception_when_repository_not_specified(self):
1043 model = VcsSettingsModel()
1041 model = VcsSettingsModel()
1044 with pytest.raises(Exception) as exc_info:
1042 with pytest.raises(Exception) as exc_info:
1045 model.create_or_update_repo_settings(data=self.FORM_DATA)
1043 model.create_or_update_repo_settings(data=self.FORM_DATA)
1046 Session().commit()
1044 Session().commit()
1047
1045
1048 assert str(exc_info.value) == 'Repository is not specified'
1046 assert str(exc_info.value) == 'Repository is not specified'
1049
1047
1050 def test_only_svn_settings_are_updated_when_type_is_svn(self, backend_svn):
1048 def test_only_svn_settings_are_updated_when_type_is_svn(self, backend_svn):
1051 repo = backend_svn.create_repo()
1049 repo = backend_svn.create_repo()
1052 model = VcsSettingsModel(repo=repo)
1050 model = VcsSettingsModel(repo=repo)
1053 with self._patch_model(model) as mocks:
1051 with self._patch_model(model) as mocks:
1054 model.create_or_update_repo_settings(
1052 model.create_or_update_repo_settings(
1055 data=self.FORM_DATA, inherit_global_settings=False)
1053 data=self.FORM_DATA, inherit_global_settings=False)
1056 Session().commit()
1054 Session().commit()
1057
1055
1058 mocks['create_repo_svn_settings'].assert_called_once_with(
1056 mocks['create_repo_svn_settings'].assert_called_once_with(
1059 self.FORM_DATA)
1057 self.FORM_DATA)
1060 non_called_methods = (
1058 non_called_methods = (
1061 'create_or_update_repo_hook_settings',
1059 'create_or_update_repo_hook_settings',
1062 'create_or_update_repo_pr_settings',
1060 'create_or_update_repo_pr_settings',
1063 'create_or_update_repo_hg_settings')
1061 'create_or_update_repo_hg_settings')
1064 for method in non_called_methods:
1062 for method in non_called_methods:
1065 assert mocks[method].call_count == 0
1063 assert mocks[method].call_count == 0
1066
1064
1067 def test_non_svn_settings_are_updated_when_type_is_hg(self, backend_hg):
1065 def test_non_svn_settings_are_updated_when_type_is_hg(self, backend_hg):
1068 repo = backend_hg.create_repo()
1066 repo = backend_hg.create_repo()
1069 model = VcsSettingsModel(repo=repo)
1067 model = VcsSettingsModel(repo=repo)
1070 with self._patch_model(model) as mocks:
1068 with self._patch_model(model) as mocks:
1071 model.create_or_update_repo_settings(
1069 model.create_or_update_repo_settings(
1072 data=self.FORM_DATA, inherit_global_settings=False)
1070 data=self.FORM_DATA, inherit_global_settings=False)
1073 Session().commit()
1071 Session().commit()
1074
1072
1075 assert mocks['create_repo_svn_settings'].call_count == 0
1073 assert mocks['create_repo_svn_settings'].call_count == 0
1076 called_methods = (
1074 called_methods = (
1077 'create_or_update_repo_hook_settings',
1075 'create_or_update_repo_hook_settings',
1078 'create_or_update_repo_pr_settings',
1076 'create_or_update_repo_pr_settings',
1079 'create_or_update_repo_hg_settings')
1077 'create_or_update_repo_hg_settings')
1080 for method in called_methods:
1078 for method in called_methods:
1081 mocks[method].assert_called_once_with(self.FORM_DATA)
1079 mocks[method].assert_called_once_with(self.FORM_DATA)
1082
1080
1083 def test_non_svn_and_hg_settings_are_updated_when_type_is_git(
1081 def test_non_svn_and_hg_settings_are_updated_when_type_is_git(
1084 self, backend_git):
1082 self, backend_git):
1085 repo = backend_git.create_repo()
1083 repo = backend_git.create_repo()
1086 model = VcsSettingsModel(repo=repo)
1084 model = VcsSettingsModel(repo=repo)
1087 with self._patch_model(model) as mocks:
1085 with self._patch_model(model) as mocks:
1088 model.create_or_update_repo_settings(
1086 model.create_or_update_repo_settings(
1089 data=self.FORM_DATA, inherit_global_settings=False)
1087 data=self.FORM_DATA, inherit_global_settings=False)
1090
1088
1091 assert mocks['create_repo_svn_settings'].call_count == 0
1089 assert mocks['create_repo_svn_settings'].call_count == 0
1092 called_methods = (
1090 called_methods = (
1093 'create_or_update_repo_hook_settings',
1091 'create_or_update_repo_hook_settings',
1094 'create_or_update_repo_pr_settings')
1092 'create_or_update_repo_pr_settings')
1095 non_called_methods = (
1093 non_called_methods = (
1096 'create_repo_svn_settings',
1094 'create_repo_svn_settings',
1097 'create_or_update_repo_hg_settings'
1095 'create_or_update_repo_hg_settings'
1098 )
1096 )
1099 for method in called_methods:
1097 for method in called_methods:
1100 mocks[method].assert_called_once_with(self.FORM_DATA)
1098 mocks[method].assert_called_once_with(self.FORM_DATA)
1101 for method in non_called_methods:
1099 for method in non_called_methods:
1102 assert mocks[method].call_count == 0
1100 assert mocks[method].call_count == 0
1103
1101
1104 def test_no_methods_are_called_when_settings_are_inherited(
1102 def test_no_methods_are_called_when_settings_are_inherited(
1105 self, backend):
1103 self, backend):
1106 repo = backend.create_repo()
1104 repo = backend.create_repo()
1107 model = VcsSettingsModel(repo=repo)
1105 model = VcsSettingsModel(repo=repo)
1108 with self._patch_model(model) as mocks:
1106 with self._patch_model(model) as mocks:
1109 model.create_or_update_repo_settings(
1107 model.create_or_update_repo_settings(
1110 data=self.FORM_DATA, inherit_global_settings=True)
1108 data=self.FORM_DATA, inherit_global_settings=True)
1111 for method_name in mocks:
1109 for method_name in mocks:
1112 assert mocks[method_name].call_count == 0
1110 assert mocks[method_name].call_count == 0
1113
1111
1114 def test_cache_is_marked_for_invalidation(self, repo_stub):
1112 def test_cache_is_marked_for_invalidation(self, repo_stub):
1115 model = VcsSettingsModel(repo=repo_stub)
1113 model = VcsSettingsModel(repo=repo_stub)
1116 invalidation_patcher = mock.patch(
1114 invalidation_patcher = mock.patch(
1117 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
1115 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
1118 with invalidation_patcher as invalidation_mock:
1116 with invalidation_patcher as invalidation_mock:
1119 model.create_or_update_repo_settings(
1117 model.create_or_update_repo_settings(
1120 data=self.FORM_DATA, inherit_global_settings=True)
1118 data=self.FORM_DATA, inherit_global_settings=True)
1121 Session().commit()
1119 Session().commit()
1122
1120
1123 invalidation_mock.assert_called_once_with(
1121 invalidation_mock.assert_called_once_with(
1124 repo_stub.repo_name, delete=True)
1122 repo_stub.repo_name, delete=True)
1125
1123
1126 def test_inherit_flag_is_saved(self, repo_stub):
1124 def test_inherit_flag_is_saved(self, repo_stub):
1127 model = VcsSettingsModel(repo=repo_stub)
1125 model = VcsSettingsModel(repo=repo_stub)
1128 model.inherit_global_settings = True
1126 model.inherit_global_settings = True
1129 with self._patch_model(model):
1127 with self._patch_model(model):
1130 model.create_or_update_repo_settings(
1128 model.create_or_update_repo_settings(
1131 data=self.FORM_DATA, inherit_global_settings=False)
1129 data=self.FORM_DATA, inherit_global_settings=False)
1132 Session().commit()
1130 Session().commit()
1133
1131
1134 assert model.inherit_global_settings is False
1132 assert model.inherit_global_settings is False
1135
1133
1136 def _patch_model(self, model):
1134 def _patch_model(self, model):
1137 return mock.patch.multiple(
1135 return mock.patch.multiple(
1138 model,
1136 model,
1139 create_repo_svn_settings=mock.DEFAULT,
1137 create_repo_svn_settings=mock.DEFAULT,
1140 create_or_update_repo_hook_settings=mock.DEFAULT,
1138 create_or_update_repo_hook_settings=mock.DEFAULT,
1141 create_or_update_repo_pr_settings=mock.DEFAULT,
1139 create_or_update_repo_pr_settings=mock.DEFAULT,
1142 create_or_update_repo_hg_settings=mock.DEFAULT)
1140 create_or_update_repo_hg_settings=mock.DEFAULT)
General Comments 0
You need to be logged in to leave comments. Login now