##// END OF EJS Templates
extensions: preparatory refactoring...
Thomas De Schampheleire -
r8420:2228102b default
parent child Browse files
Show More
@@ -1,596 +1,597 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
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 General Public License
12 # You should have received a copy of the GNU 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 kallithea.lib.utils
15 kallithea.lib.utils
16 ~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~
17
17
18 Utilities library for Kallithea
18 Utilities library for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 18, 2010
22 :created_on: Apr 18, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import datetime
28 import datetime
29 import logging
29 import logging
30 import os
30 import os
31 import re
31 import re
32 import sys
32 import sys
33 import traceback
33 import traceback
34 import urllib.error
34 import urllib.error
35 from distutils.version import StrictVersion
35 from distutils.version import StrictVersion
36
36
37 import mercurial.config
37 import mercurial.config
38 import mercurial.error
38 import mercurial.error
39 import mercurial.ui
39 import mercurial.ui
40
40
41 import kallithea.config.conf
41 import kallithea.config.conf
42 from kallithea.lib.exceptions import InvalidCloneUriException
42 from kallithea.lib.exceptions import InvalidCloneUriException
43 from kallithea.lib.utils2 import ascii_bytes, aslist, get_current_authuser, safe_bytes, safe_str
43 from kallithea.lib.utils2 import ascii_bytes, aslist, get_current_authuser, safe_bytes, safe_str
44 from kallithea.lib.vcs.backends.git.repository import GitRepository
44 from kallithea.lib.vcs.backends.git.repository import GitRepository
45 from kallithea.lib.vcs.backends.hg.repository import MercurialRepository
45 from kallithea.lib.vcs.backends.hg.repository import MercurialRepository
46 from kallithea.lib.vcs.conf import settings
46 from kallithea.lib.vcs.conf import settings
47 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError
47 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError
48 from kallithea.lib.vcs.utils.fakemod import create_module
48 from kallithea.lib.vcs.utils.fakemod import create_module
49 from kallithea.lib.vcs.utils.helpers import get_scm
49 from kallithea.lib.vcs.utils.helpers import get_scm
50 from kallithea.model import db, meta
50 from kallithea.model import db, meta
51 from kallithea.model.db import RepoGroup, Repository, Setting, Ui, User, UserGroup, UserLog
51 from kallithea.model.db import RepoGroup, Repository, Setting, Ui, User, UserGroup, UserLog
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*')
56 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*')
57
57
58
58
59 #==============================================================================
59 #==============================================================================
60 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
60 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
61 #==============================================================================
61 #==============================================================================
62 def get_repo_slug(request):
62 def get_repo_slug(request):
63 _repo = request.environ['pylons.routes_dict'].get('repo_name')
63 _repo = request.environ['pylons.routes_dict'].get('repo_name')
64 if _repo:
64 if _repo:
65 _repo = _repo.rstrip('/')
65 _repo = _repo.rstrip('/')
66 return _repo
66 return _repo
67
67
68
68
69 def get_repo_group_slug(request):
69 def get_repo_group_slug(request):
70 _group = request.environ['pylons.routes_dict'].get('group_name')
70 _group = request.environ['pylons.routes_dict'].get('group_name')
71 if _group:
71 if _group:
72 _group = _group.rstrip('/')
72 _group = _group.rstrip('/')
73 return _group
73 return _group
74
74
75
75
76 def get_user_group_slug(request):
76 def get_user_group_slug(request):
77 _group = request.environ['pylons.routes_dict'].get('id')
77 _group = request.environ['pylons.routes_dict'].get('id')
78 _group = UserGroup.get(_group)
78 _group = UserGroup.get(_group)
79 if _group:
79 if _group:
80 return _group.users_group_name
80 return _group.users_group_name
81 return None
81 return None
82
82
83
83
84 def _get_permanent_id(s):
84 def _get_permanent_id(s):
85 """Helper for decoding stable URLs with repo ID. For a string like '_123'
85 """Helper for decoding stable URLs with repo ID. For a string like '_123'
86 return 123.
86 return 123.
87 """
87 """
88 by_id_match = re.match(r'^_(\d+)$', s)
88 by_id_match = re.match(r'^_(\d+)$', s)
89 if by_id_match is None:
89 if by_id_match is None:
90 return None
90 return None
91 return int(by_id_match.group(1))
91 return int(by_id_match.group(1))
92
92
93
93
94 def fix_repo_id_name(path):
94 def fix_repo_id_name(path):
95 """
95 """
96 Rewrite repo_name for _<ID> permanent URLs.
96 Rewrite repo_name for _<ID> permanent URLs.
97
97
98 Given a path, if the first path element is like _<ID>, return the path with
98 Given a path, if the first path element is like _<ID>, return the path with
99 this part expanded to the corresponding full repo name, else return the
99 this part expanded to the corresponding full repo name, else return the
100 provided path.
100 provided path.
101 """
101 """
102 first, rest = path, ''
102 first, rest = path, ''
103 if '/' in path:
103 if '/' in path:
104 first, rest_ = path.split('/', 1)
104 first, rest_ = path.split('/', 1)
105 rest = '/' + rest_
105 rest = '/' + rest_
106 repo_id = _get_permanent_id(first)
106 repo_id = _get_permanent_id(first)
107 if repo_id is not None:
107 if repo_id is not None:
108 repo = Repository.get(repo_id)
108 repo = Repository.get(repo_id)
109 if repo is not None:
109 if repo is not None:
110 return repo.repo_name + rest
110 return repo.repo_name + rest
111 return path
111 return path
112
112
113
113
114 def action_logger(user, action, repo, ipaddr='', commit=False):
114 def action_logger(user, action, repo, ipaddr='', commit=False):
115 """
115 """
116 Action logger for various actions made by users
116 Action logger for various actions made by users
117
117
118 :param user: user that made this action, can be a unique username string or
118 :param user: user that made this action, can be a unique username string or
119 object containing user_id attribute
119 object containing user_id attribute
120 :param action: action to log, should be on of predefined unique actions for
120 :param action: action to log, should be on of predefined unique actions for
121 easy translations
121 easy translations
122 :param repo: string name of repository or object containing repo_id,
122 :param repo: string name of repository or object containing repo_id,
123 that action was made on
123 that action was made on
124 :param ipaddr: optional IP address from what the action was made
124 :param ipaddr: optional IP address from what the action was made
125
125
126 """
126 """
127
127
128 # if we don't get explicit IP address try to get one from registered user
128 # if we don't get explicit IP address try to get one from registered user
129 # in tmpl context var
129 # in tmpl context var
130 if not ipaddr:
130 if not ipaddr:
131 ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
131 ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
132
132
133 if getattr(user, 'user_id', None):
133 if getattr(user, 'user_id', None):
134 user_obj = User.get(user.user_id)
134 user_obj = User.get(user.user_id)
135 elif isinstance(user, str):
135 elif isinstance(user, str):
136 user_obj = User.get_by_username(user)
136 user_obj = User.get_by_username(user)
137 else:
137 else:
138 raise Exception('You have to provide a user object or a username')
138 raise Exception('You have to provide a user object or a username')
139
139
140 if getattr(repo, 'repo_id', None):
140 if getattr(repo, 'repo_id', None):
141 repo_obj = Repository.get(repo.repo_id)
141 repo_obj = Repository.get(repo.repo_id)
142 repo_name = repo_obj.repo_name
142 repo_name = repo_obj.repo_name
143 elif isinstance(repo, str):
143 elif isinstance(repo, str):
144 repo_name = repo.lstrip('/')
144 repo_name = repo.lstrip('/')
145 repo_obj = Repository.get_by_repo_name(repo_name)
145 repo_obj = Repository.get_by_repo_name(repo_name)
146 else:
146 else:
147 repo_obj = None
147 repo_obj = None
148 repo_name = ''
148 repo_name = ''
149
149
150 user_log = UserLog()
150 user_log = UserLog()
151 user_log.user_id = user_obj.user_id
151 user_log.user_id = user_obj.user_id
152 user_log.username = user_obj.username
152 user_log.username = user_obj.username
153 user_log.action = action
153 user_log.action = action
154
154
155 user_log.repository = repo_obj
155 user_log.repository = repo_obj
156 user_log.repository_name = repo_name
156 user_log.repository_name = repo_name
157
157
158 user_log.action_date = datetime.datetime.now()
158 user_log.action_date = datetime.datetime.now()
159 user_log.user_ip = ipaddr
159 user_log.user_ip = ipaddr
160 meta.Session().add(user_log)
160 meta.Session().add(user_log)
161
161
162 log.info('Logging action:%s on %s by user:%s ip:%s',
162 log.info('Logging action:%s on %s by user:%s ip:%s',
163 action, repo, user_obj, ipaddr)
163 action, repo, user_obj, ipaddr)
164 if commit:
164 if commit:
165 meta.Session().commit()
165 meta.Session().commit()
166
166
167
167
168 def get_filesystem_repos(path):
168 def get_filesystem_repos(path):
169 """
169 """
170 Scans given path for repos and return (name,(type,path)) tuple
170 Scans given path for repos and return (name,(type,path)) tuple
171
171
172 :param path: path to scan for repositories
172 :param path: path to scan for repositories
173 :param recursive: recursive search and return names with subdirs in front
173 :param recursive: recursive search and return names with subdirs in front
174 """
174 """
175
175
176 # remove ending slash for better results
176 # remove ending slash for better results
177 path = path.rstrip(os.sep)
177 path = path.rstrip(os.sep)
178 log.debug('now scanning in %s', path)
178 log.debug('now scanning in %s', path)
179
179
180 def isdir(*n):
180 def isdir(*n):
181 return os.path.isdir(os.path.join(*n))
181 return os.path.isdir(os.path.join(*n))
182
182
183 for root, dirs, _files in os.walk(path):
183 for root, dirs, _files in os.walk(path):
184 recurse_dirs = []
184 recurse_dirs = []
185 for subdir in dirs:
185 for subdir in dirs:
186 # skip removed repos
186 # skip removed repos
187 if REMOVED_REPO_PAT.match(subdir):
187 if REMOVED_REPO_PAT.match(subdir):
188 continue
188 continue
189
189
190 # skip .<something> dirs TODO: rly? then we should prevent creating them ...
190 # skip .<something> dirs TODO: rly? then we should prevent creating them ...
191 if subdir.startswith('.'):
191 if subdir.startswith('.'):
192 continue
192 continue
193
193
194 cur_path = os.path.join(root, subdir)
194 cur_path = os.path.join(root, subdir)
195 if isdir(cur_path, '.git'):
195 if isdir(cur_path, '.git'):
196 log.warning('ignoring non-bare Git repo: %s', cur_path)
196 log.warning('ignoring non-bare Git repo: %s', cur_path)
197 continue
197 continue
198
198
199 if (isdir(cur_path, '.hg') or
199 if (isdir(cur_path, '.hg') or
200 isdir(cur_path, '.svn') or
200 isdir(cur_path, '.svn') or
201 isdir(cur_path, 'objects') and (isdir(cur_path, 'refs') or
201 isdir(cur_path, 'objects') and (isdir(cur_path, 'refs') or
202 os.path.isfile(os.path.join(cur_path, 'packed-refs')))):
202 os.path.isfile(os.path.join(cur_path, 'packed-refs')))):
203
203
204 if not os.access(cur_path, os.R_OK) or not os.access(cur_path, os.X_OK):
204 if not os.access(cur_path, os.R_OK) or not os.access(cur_path, os.X_OK):
205 log.warning('ignoring repo path without access: %s', cur_path)
205 log.warning('ignoring repo path without access: %s', cur_path)
206 continue
206 continue
207
207
208 if not os.access(cur_path, os.W_OK):
208 if not os.access(cur_path, os.W_OK):
209 log.warning('repo path without write access: %s', cur_path)
209 log.warning('repo path without write access: %s', cur_path)
210
210
211 try:
211 try:
212 scm_info = get_scm(cur_path)
212 scm_info = get_scm(cur_path)
213 assert cur_path.startswith(path)
213 assert cur_path.startswith(path)
214 repo_path = cur_path[len(path) + 1:]
214 repo_path = cur_path[len(path) + 1:]
215 yield repo_path, scm_info
215 yield repo_path, scm_info
216 continue # no recursion
216 continue # no recursion
217 except VCSError:
217 except VCSError:
218 # We should perhaps ignore such broken repos, but especially
218 # We should perhaps ignore such broken repos, but especially
219 # the bare git detection is unreliable so we dive into it
219 # the bare git detection is unreliable so we dive into it
220 pass
220 pass
221
221
222 recurse_dirs.append(subdir)
222 recurse_dirs.append(subdir)
223
223
224 dirs[:] = recurse_dirs
224 dirs[:] = recurse_dirs
225
225
226
226
227 def is_valid_repo_uri(repo_type, url, ui):
227 def is_valid_repo_uri(repo_type, url, ui):
228 """Check if the url seems like a valid remote repo location
228 """Check if the url seems like a valid remote repo location
229 Raise InvalidCloneUriException if any problems"""
229 Raise InvalidCloneUriException if any problems"""
230 if repo_type == 'hg':
230 if repo_type == 'hg':
231 if url.startswith('http') or url.startswith('ssh'):
231 if url.startswith('http') or url.startswith('ssh'):
232 # initially check if it's at least the proper URL
232 # initially check if it's at least the proper URL
233 # or does it pass basic auth
233 # or does it pass basic auth
234 try:
234 try:
235 MercurialRepository._check_url(url, ui)
235 MercurialRepository._check_url(url, ui)
236 except urllib.error.URLError as e:
236 except urllib.error.URLError as e:
237 raise InvalidCloneUriException('URI %s URLError: %s' % (url, e))
237 raise InvalidCloneUriException('URI %s URLError: %s' % (url, e))
238 except mercurial.error.RepoError as e:
238 except mercurial.error.RepoError as e:
239 raise InvalidCloneUriException('Mercurial %s: %s' % (type(e).__name__, safe_str(bytes(e))))
239 raise InvalidCloneUriException('Mercurial %s: %s' % (type(e).__name__, safe_str(bytes(e))))
240 elif url.startswith('svn+http'):
240 elif url.startswith('svn+http'):
241 try:
241 try:
242 from hgsubversion.svnrepo import svnremoterepo
242 from hgsubversion.svnrepo import svnremoterepo
243 except ImportError:
243 except ImportError:
244 raise InvalidCloneUriException('URI type %s not supported - hgsubversion is not available' % (url,))
244 raise InvalidCloneUriException('URI type %s not supported - hgsubversion is not available' % (url,))
245 svnremoterepo(ui, url).svn.uuid
245 svnremoterepo(ui, url).svn.uuid
246 elif url.startswith('git+http'):
246 elif url.startswith('git+http'):
247 raise InvalidCloneUriException('URI type %s not implemented' % (url,))
247 raise InvalidCloneUriException('URI type %s not implemented' % (url,))
248 else:
248 else:
249 raise InvalidCloneUriException('URI %s not allowed' % (url,))
249 raise InvalidCloneUriException('URI %s not allowed' % (url,))
250
250
251 elif repo_type == 'git':
251 elif repo_type == 'git':
252 if url.startswith('http') or url.startswith('git'):
252 if url.startswith('http') or url.startswith('git'):
253 # initially check if it's at least the proper URL
253 # initially check if it's at least the proper URL
254 # or does it pass basic auth
254 # or does it pass basic auth
255 try:
255 try:
256 GitRepository._check_url(url)
256 GitRepository._check_url(url)
257 except urllib.error.URLError as e:
257 except urllib.error.URLError as e:
258 raise InvalidCloneUriException('URI %s URLError: %s' % (url, e))
258 raise InvalidCloneUriException('URI %s URLError: %s' % (url, e))
259 elif url.startswith('svn+http'):
259 elif url.startswith('svn+http'):
260 raise InvalidCloneUriException('URI type %s not implemented' % (url,))
260 raise InvalidCloneUriException('URI type %s not implemented' % (url,))
261 elif url.startswith('hg+http'):
261 elif url.startswith('hg+http'):
262 raise InvalidCloneUriException('URI type %s not implemented' % (url,))
262 raise InvalidCloneUriException('URI type %s not implemented' % (url,))
263 else:
263 else:
264 raise InvalidCloneUriException('URI %s not allowed' % (url))
264 raise InvalidCloneUriException('URI %s not allowed' % (url))
265
265
266
266
267 def is_valid_repo(repo_name, base_path, scm=None):
267 def is_valid_repo(repo_name, base_path, scm=None):
268 """
268 """
269 Returns True if given path is a valid repository False otherwise.
269 Returns True if given path is a valid repository False otherwise.
270 If scm param is given also compare if given scm is the same as expected
270 If scm param is given also compare if given scm is the same as expected
271 from scm parameter
271 from scm parameter
272
272
273 :param repo_name:
273 :param repo_name:
274 :param base_path:
274 :param base_path:
275 :param scm:
275 :param scm:
276
276
277 :return True: if given path is a valid repository
277 :return True: if given path is a valid repository
278 """
278 """
279 # TODO: paranoid security checks?
279 # TODO: paranoid security checks?
280 full_path = os.path.join(base_path, repo_name)
280 full_path = os.path.join(base_path, repo_name)
281
281
282 try:
282 try:
283 scm_ = get_scm(full_path)
283 scm_ = get_scm(full_path)
284 if scm:
284 if scm:
285 return scm_[0] == scm
285 return scm_[0] == scm
286 return True
286 return True
287 except VCSError:
287 except VCSError:
288 return False
288 return False
289
289
290
290
291 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
291 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
292 """
292 """
293 Returns True if given path is a repository group False otherwise
293 Returns True if given path is a repository group False otherwise
294
294
295 :param repo_name:
295 :param repo_name:
296 :param base_path:
296 :param base_path:
297 """
297 """
298 full_path = os.path.join(base_path, repo_group_name)
298 full_path = os.path.join(base_path, repo_group_name)
299
299
300 # check if it's not a repo
300 # check if it's not a repo
301 if is_valid_repo(repo_group_name, base_path):
301 if is_valid_repo(repo_group_name, base_path):
302 return False
302 return False
303
303
304 try:
304 try:
305 # we need to check bare git repos at higher level
305 # we need to check bare git repos at higher level
306 # since we might match branches/hooks/info/objects or possible
306 # since we might match branches/hooks/info/objects or possible
307 # other things inside bare git repo
307 # other things inside bare git repo
308 get_scm(os.path.dirname(full_path))
308 get_scm(os.path.dirname(full_path))
309 return False
309 return False
310 except VCSError:
310 except VCSError:
311 pass
311 pass
312
312
313 # check if it's a valid path
313 # check if it's a valid path
314 if skip_path_check or os.path.isdir(full_path):
314 if skip_path_check or os.path.isdir(full_path):
315 return True
315 return True
316
316
317 return False
317 return False
318
318
319
319
320 def make_ui(repo_path=None):
320 def make_ui(repo_path=None):
321 """
321 """
322 Create an Mercurial 'ui' object based on database Ui settings, possibly
322 Create an Mercurial 'ui' object based on database Ui settings, possibly
323 augmenting with content from a hgrc file.
323 augmenting with content from a hgrc file.
324 """
324 """
325 baseui = mercurial.ui.ui()
325 baseui = mercurial.ui.ui()
326
326
327 # clean the baseui object
327 # clean the baseui object
328 baseui._ocfg = mercurial.config.config()
328 baseui._ocfg = mercurial.config.config()
329 baseui._ucfg = mercurial.config.config()
329 baseui._ucfg = mercurial.config.config()
330 baseui._tcfg = mercurial.config.config()
330 baseui._tcfg = mercurial.config.config()
331
331
332 sa = meta.Session()
332 sa = meta.Session()
333 for ui_ in sa.query(Ui).order_by(Ui.ui_section, Ui.ui_key):
333 for ui_ in sa.query(Ui).order_by(Ui.ui_section, Ui.ui_key):
334 if ui_.ui_active:
334 if ui_.ui_active:
335 log.debug('config from db: [%s] %s=%r', ui_.ui_section,
335 log.debug('config from db: [%s] %s=%r', ui_.ui_section,
336 ui_.ui_key, ui_.ui_value)
336 ui_.ui_key, ui_.ui_value)
337 baseui.setconfig(ascii_bytes(ui_.ui_section), ascii_bytes(ui_.ui_key),
337 baseui.setconfig(ascii_bytes(ui_.ui_section), ascii_bytes(ui_.ui_key),
338 b'' if ui_.ui_value is None else safe_bytes(ui_.ui_value))
338 b'' if ui_.ui_value is None else safe_bytes(ui_.ui_value))
339
339
340 # force set push_ssl requirement to False, Kallithea handles that
340 # force set push_ssl requirement to False, Kallithea handles that
341 baseui.setconfig(b'web', b'push_ssl', False)
341 baseui.setconfig(b'web', b'push_ssl', False)
342 baseui.setconfig(b'web', b'allow_push', b'*')
342 baseui.setconfig(b'web', b'allow_push', b'*')
343 # prevent interactive questions for ssh password / passphrase
343 # prevent interactive questions for ssh password / passphrase
344 ssh = baseui.config(b'ui', b'ssh', default=b'ssh')
344 ssh = baseui.config(b'ui', b'ssh', default=b'ssh')
345 baseui.setconfig(b'ui', b'ssh', b'%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
345 baseui.setconfig(b'ui', b'ssh', b'%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
346 # push / pull hooks
346 # push / pull hooks
347 baseui.setconfig(b'hooks', b'changegroup.kallithea_log_push_action', b'python:kallithea.lib.hooks.log_push_action')
347 baseui.setconfig(b'hooks', b'changegroup.kallithea_log_push_action', b'python:kallithea.lib.hooks.log_push_action')
348 baseui.setconfig(b'hooks', b'outgoing.kallithea_log_pull_action', b'python:kallithea.lib.hooks.log_pull_action')
348 baseui.setconfig(b'hooks', b'outgoing.kallithea_log_pull_action', b'python:kallithea.lib.hooks.log_pull_action')
349
349
350 if repo_path is not None:
350 if repo_path is not None:
351 # Note: MercurialRepository / mercurial.localrepo.instance will do this too, so it will always be possible to override db settings or what is hardcoded above
351 # Note: MercurialRepository / mercurial.localrepo.instance will do this too, so it will always be possible to override db settings or what is hardcoded above
352 baseui.readconfig(repo_path)
352 baseui.readconfig(repo_path)
353
353
354 assert baseui.plain() # set by hgcompat.monkey_do (invoked from import of vcs.backends.hg) to minimize potential impact of loading config files
354 assert baseui.plain() # set by hgcompat.monkey_do (invoked from import of vcs.backends.hg) to minimize potential impact of loading config files
355 return baseui
355 return baseui
356
356
357
357
358 def set_app_settings(config):
358 def set_app_settings(config):
359 """
359 """
360 Updates app config with new settings from database
360 Updates app config with new settings from database
361
361
362 :param config:
362 :param config:
363 """
363 """
364 hgsettings = Setting.get_app_settings()
364 hgsettings = Setting.get_app_settings()
365 for k, v in hgsettings.items():
365 for k, v in hgsettings.items():
366 config[k] = v
366 config[k] = v
367 config['base_path'] = Ui.get_repos_location()
367 config['base_path'] = Ui.get_repos_location()
368
368
369
369
370 def set_vcs_config(config):
370 def set_vcs_config(config):
371 """
371 """
372 Patch VCS config with some Kallithea specific stuff
372 Patch VCS config with some Kallithea specific stuff
373
373
374 :param config: kallithea.CONFIG
374 :param config: kallithea.CONFIG
375 """
375 """
376 settings.BACKENDS = {
376 settings.BACKENDS = {
377 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
377 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
378 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
378 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
379 }
379 }
380
380
381 settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
381 settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
382 settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
382 settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
383 settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
383 settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
384 'utf-8'), sep=',')
384 'utf-8'), sep=',')
385
385
386
386
387 def set_indexer_config(config):
387 def set_indexer_config(config):
388 """
388 """
389 Update Whoosh index mapping
389 Update Whoosh index mapping
390
390
391 :param config: kallithea.CONFIG
391 :param config: kallithea.CONFIG
392 """
392 """
393 log.debug('adding extra into INDEX_EXTENSIONS')
393 log.debug('adding extra into INDEX_EXTENSIONS')
394 kallithea.config.conf.INDEX_EXTENSIONS.extend(re.split(r'\s+', config.get('index.extensions', '')))
394 kallithea.config.conf.INDEX_EXTENSIONS.extend(re.split(r'\s+', config.get('index.extensions', '')))
395
395
396 log.debug('adding extra into INDEX_FILENAMES')
396 log.debug('adding extra into INDEX_FILENAMES')
397 kallithea.config.conf.INDEX_FILENAMES.extend(re.split(r'\s+', config.get('index.filenames', '')))
397 kallithea.config.conf.INDEX_FILENAMES.extend(re.split(r'\s+', config.get('index.filenames', '')))
398
398
399
399
400 def map_groups(path):
400 def map_groups(path):
401 """
401 """
402 Given a full path to a repository, create all nested groups that this
402 Given a full path to a repository, create all nested groups that this
403 repo is inside. This function creates parent-child relationships between
403 repo is inside. This function creates parent-child relationships between
404 groups and creates default perms for all new groups.
404 groups and creates default perms for all new groups.
405
405
406 :param paths: full path to repository
406 :param paths: full path to repository
407 """
407 """
408 from kallithea.model.repo_group import RepoGroupModel
408 from kallithea.model.repo_group import RepoGroupModel
409 sa = meta.Session()
409 sa = meta.Session()
410 groups = path.split(db.URL_SEP)
410 groups = path.split(db.URL_SEP)
411 parent = None
411 parent = None
412 group = None
412 group = None
413
413
414 # last element is repo in nested groups structure
414 # last element is repo in nested groups structure
415 groups = groups[:-1]
415 groups = groups[:-1]
416 rgm = RepoGroupModel()
416 rgm = RepoGroupModel()
417 owner = User.get_first_admin()
417 owner = User.get_first_admin()
418 for lvl, group_name in enumerate(groups):
418 for lvl, group_name in enumerate(groups):
419 group_name = '/'.join(groups[:lvl] + [group_name])
419 group_name = '/'.join(groups[:lvl] + [group_name])
420 group = RepoGroup.get_by_group_name(group_name)
420 group = RepoGroup.get_by_group_name(group_name)
421 desc = '%s group' % group_name
421 desc = '%s group' % group_name
422
422
423 # skip folders that are now removed repos
423 # skip folders that are now removed repos
424 if REMOVED_REPO_PAT.match(group_name):
424 if REMOVED_REPO_PAT.match(group_name):
425 break
425 break
426
426
427 if group is None:
427 if group is None:
428 log.debug('creating group level: %s group_name: %s',
428 log.debug('creating group level: %s group_name: %s',
429 lvl, group_name)
429 lvl, group_name)
430 group = RepoGroup(group_name, parent)
430 group = RepoGroup(group_name, parent)
431 group.group_description = desc
431 group.group_description = desc
432 group.owner = owner
432 group.owner = owner
433 sa.add(group)
433 sa.add(group)
434 rgm._create_default_perms(group)
434 rgm._create_default_perms(group)
435 sa.flush()
435 sa.flush()
436
436
437 parent = group
437 parent = group
438 return group
438 return group
439
439
440
440
441 def repo2db_mapper(initial_repo_dict, remove_obsolete=False,
441 def repo2db_mapper(initial_repo_dict, remove_obsolete=False,
442 install_git_hooks=False, user=None, overwrite_git_hooks=False):
442 install_git_hooks=False, user=None, overwrite_git_hooks=False):
443 """
443 """
444 maps all repos given in initial_repo_dict, non existing repositories
444 maps all repos given in initial_repo_dict, non existing repositories
445 are created, if remove_obsolete is True it also check for db entries
445 are created, if remove_obsolete is True it also check for db entries
446 that are not in initial_repo_dict and removes them.
446 that are not in initial_repo_dict and removes them.
447
447
448 :param initial_repo_dict: mapping with repositories found by scanning methods
448 :param initial_repo_dict: mapping with repositories found by scanning methods
449 :param remove_obsolete: check for obsolete entries in database
449 :param remove_obsolete: check for obsolete entries in database
450 :param install_git_hooks: if this is True, also check and install git hook
450 :param install_git_hooks: if this is True, also check and install git hook
451 for a repo if missing
451 for a repo if missing
452 :param overwrite_git_hooks: if this is True, overwrite any existing git hooks
452 :param overwrite_git_hooks: if this is True, overwrite any existing git hooks
453 that may be encountered (even if user-deployed)
453 that may be encountered (even if user-deployed)
454 """
454 """
455 from kallithea.model.repo import RepoModel
455 from kallithea.model.repo import RepoModel
456 from kallithea.model.scm import ScmModel
456 from kallithea.model.scm import ScmModel
457 sa = meta.Session()
457 sa = meta.Session()
458 repo_model = RepoModel()
458 repo_model = RepoModel()
459 if user is None:
459 if user is None:
460 user = User.get_first_admin()
460 user = User.get_first_admin()
461 added = []
461 added = []
462
462
463 # creation defaults
463 # creation defaults
464 defs = Setting.get_default_repo_settings(strip_prefix=True)
464 defs = Setting.get_default_repo_settings(strip_prefix=True)
465 enable_statistics = defs.get('repo_enable_statistics')
465 enable_statistics = defs.get('repo_enable_statistics')
466 enable_downloads = defs.get('repo_enable_downloads')
466 enable_downloads = defs.get('repo_enable_downloads')
467 private = defs.get('repo_private')
467 private = defs.get('repo_private')
468
468
469 for name, repo in sorted(initial_repo_dict.items()):
469 for name, repo in sorted(initial_repo_dict.items()):
470 group = map_groups(name)
470 group = map_groups(name)
471 db_repo = repo_model.get_by_repo_name(name)
471 db_repo = repo_model.get_by_repo_name(name)
472 # found repo that is on filesystem not in Kallithea database
472 # found repo that is on filesystem not in Kallithea database
473 if not db_repo:
473 if not db_repo:
474 log.info('repository %s not found, creating now', name)
474 log.info('repository %s not found, creating now', name)
475 added.append(name)
475 added.append(name)
476 desc = (repo.description
476 desc = (repo.description
477 if repo.description != 'unknown'
477 if repo.description != 'unknown'
478 else '%s repository' % name)
478 else '%s repository' % name)
479
479
480 new_repo = repo_model._create_repo(
480 new_repo = repo_model._create_repo(
481 repo_name=name,
481 repo_name=name,
482 repo_type=repo.alias,
482 repo_type=repo.alias,
483 description=desc,
483 description=desc,
484 repo_group=getattr(group, 'group_id', None),
484 repo_group=getattr(group, 'group_id', None),
485 owner=user,
485 owner=user,
486 enable_downloads=enable_downloads,
486 enable_downloads=enable_downloads,
487 enable_statistics=enable_statistics,
487 enable_statistics=enable_statistics,
488 private=private,
488 private=private,
489 state=Repository.STATE_CREATED
489 state=Repository.STATE_CREATED
490 )
490 )
491 sa.commit()
491 sa.commit()
492 # we added that repo just now, and make sure it has githook
492 # we added that repo just now, and make sure it has githook
493 # installed, and updated server info
493 # installed, and updated server info
494 if new_repo.repo_type == 'git':
494 if new_repo.repo_type == 'git':
495 git_repo = new_repo.scm_instance
495 git_repo = new_repo.scm_instance
496 ScmModel().install_git_hooks(git_repo)
496 ScmModel().install_git_hooks(git_repo)
497 # update repository server-info
497 # update repository server-info
498 log.debug('Running update server info')
498 log.debug('Running update server info')
499 git_repo._update_server_info()
499 git_repo._update_server_info()
500 new_repo.update_changeset_cache()
500 new_repo.update_changeset_cache()
501 elif install_git_hooks:
501 elif install_git_hooks:
502 if db_repo.repo_type == 'git':
502 if db_repo.repo_type == 'git':
503 ScmModel().install_git_hooks(db_repo.scm_instance, force=overwrite_git_hooks)
503 ScmModel().install_git_hooks(db_repo.scm_instance, force=overwrite_git_hooks)
504
504
505 removed = []
505 removed = []
506 # remove from database those repositories that are not in the filesystem
506 # remove from database those repositories that are not in the filesystem
507 for repo in sa.query(Repository).all():
507 for repo in sa.query(Repository).all():
508 if repo.repo_name not in initial_repo_dict:
508 if repo.repo_name not in initial_repo_dict:
509 if remove_obsolete:
509 if remove_obsolete:
510 log.debug("Removing non-existing repository found in db `%s`",
510 log.debug("Removing non-existing repository found in db `%s`",
511 repo.repo_name)
511 repo.repo_name)
512 try:
512 try:
513 RepoModel().delete(repo, forks='detach', fs_remove=False)
513 RepoModel().delete(repo, forks='detach', fs_remove=False)
514 sa.commit()
514 sa.commit()
515 except Exception:
515 except Exception:
516 #don't hold further removals on error
516 #don't hold further removals on error
517 log.error(traceback.format_exc())
517 log.error(traceback.format_exc())
518 sa.rollback()
518 sa.rollback()
519 removed.append(repo.repo_name)
519 removed.append(repo.repo_name)
520 return added, removed
520 return added, removed
521
521
522
522
523 def load_extensions(root_path):
523 def load_extensions(root_path):
524 path = os.path.join(root_path, 'rcextensions', '__init__.py')
524 try:
525 if os.path.isfile(path):
525 ext = create_module('rc', os.path.join(root_path, 'rcextensions', '__init__.py'))
526 ext = create_module('rc', path)
526 except FileNotFoundError:
527 kallithea.EXTENSIONS = ext
527 return
528 log.debug('Found rcextensions now loading %s...', ext)
529
528
530 # Additional mappings that are not present in the pygments lexers
529 log.info('Loaded rcextensions from %s', ext)
531 kallithea.config.conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(ext, 'EXTRA_MAPPINGS', {}))
530 kallithea.EXTENSIONS = ext
532
531
533 # OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
532 # Additional mappings that are not present in the pygments lexers
533 kallithea.config.conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(ext, 'EXTRA_MAPPINGS', {}))
534
534
535 if getattr(ext, 'INDEX_EXTENSIONS', []):
535 # Override any INDEX_EXTENSIONS
536 log.debug('settings custom INDEX_EXTENSIONS')
536 if getattr(ext, 'INDEX_EXTENSIONS', []):
537 kallithea.config.conf.INDEX_EXTENSIONS = getattr(ext, 'INDEX_EXTENSIONS', [])
537 log.debug('settings custom INDEX_EXTENSIONS')
538 kallithea.config.conf.INDEX_EXTENSIONS = getattr(ext, 'INDEX_EXTENSIONS', [])
538
539
539 # ADDITIONAL MAPPINGS
540 # Additional INDEX_EXTENSIONS
540 log.debug('adding extra into INDEX_EXTENSIONS')
541 log.debug('adding extra into INDEX_EXTENSIONS')
541 kallithea.config.conf.INDEX_EXTENSIONS.extend(getattr(ext, 'EXTRA_INDEX_EXTENSIONS', []))
542 kallithea.config.conf.INDEX_EXTENSIONS.extend(getattr(ext, 'EXTRA_INDEX_EXTENSIONS', []))
542
543
543
544
544 #==============================================================================
545 #==============================================================================
545 # MISC
546 # MISC
546 #==============================================================================
547 #==============================================================================
547
548
548 git_req_ver = StrictVersion('1.7.4')
549 git_req_ver = StrictVersion('1.7.4')
549
550
550 def check_git_version():
551 def check_git_version():
551 """
552 """
552 Checks what version of git is installed on the system, and raise a system exit
553 Checks what version of git is installed on the system, and raise a system exit
553 if it's too old for Kallithea to work properly.
554 if it's too old for Kallithea to work properly.
554 """
555 """
555 if 'git' not in kallithea.BACKENDS:
556 if 'git' not in kallithea.BACKENDS:
556 return None
557 return None
557
558
558 if not settings.GIT_EXECUTABLE_PATH:
559 if not settings.GIT_EXECUTABLE_PATH:
559 log.warning('No git executable configured - check "git_path" in the ini file.')
560 log.warning('No git executable configured - check "git_path" in the ini file.')
560 return None
561 return None
561
562
562 try:
563 try:
563 stdout, stderr = GitRepository._run_git_command(['--version'])
564 stdout, stderr = GitRepository._run_git_command(['--version'])
564 except RepositoryError as e:
565 except RepositoryError as e:
565 # message will already have been logged as error
566 # message will already have been logged as error
566 log.warning('No working git executable found - check "git_path" in the ini file.')
567 log.warning('No working git executable found - check "git_path" in the ini file.')
567 return None
568 return None
568
569
569 if stderr:
570 if stderr:
570 log.warning('Error/stderr from "%s --version":\n%s', settings.GIT_EXECUTABLE_PATH, safe_str(stderr))
571 log.warning('Error/stderr from "%s --version":\n%s', settings.GIT_EXECUTABLE_PATH, safe_str(stderr))
571
572
572 if not stdout:
573 if not stdout:
573 log.warning('No working git executable found - check "git_path" in the ini file.')
574 log.warning('No working git executable found - check "git_path" in the ini file.')
574 return None
575 return None
575
576
576 output = safe_str(stdout).strip()
577 output = safe_str(stdout).strip()
577 m = re.search(r"\d+.\d+.\d+", output)
578 m = re.search(r"\d+.\d+.\d+", output)
578 if m:
579 if m:
579 ver = StrictVersion(m.group(0))
580 ver = StrictVersion(m.group(0))
580 log.debug('Git executable: "%s", version %s (parsed from: "%s")',
581 log.debug('Git executable: "%s", version %s (parsed from: "%s")',
581 settings.GIT_EXECUTABLE_PATH, ver, output)
582 settings.GIT_EXECUTABLE_PATH, ver, output)
582 if ver < git_req_ver:
583 if ver < git_req_ver:
583 log.error('Kallithea detected %s version %s, which is too old '
584 log.error('Kallithea detected %s version %s, which is too old '
584 'for the system to function properly. '
585 'for the system to function properly. '
585 'Please upgrade to version %s or later. '
586 'Please upgrade to version %s or later. '
586 'If you strictly need Mercurial repositories, you can '
587 'If you strictly need Mercurial repositories, you can '
587 'clear the "git_path" setting in the ini file.',
588 'clear the "git_path" setting in the ini file.',
588 settings.GIT_EXECUTABLE_PATH, ver, git_req_ver)
589 settings.GIT_EXECUTABLE_PATH, ver, git_req_ver)
589 log.error("Terminating ...")
590 log.error("Terminating ...")
590 sys.exit(1)
591 sys.exit(1)
591 else:
592 else:
592 ver = StrictVersion('0.0.0')
593 ver = StrictVersion('0.0.0')
593 log.warning('Error finding version number in "%s --version" stdout:\n%s',
594 log.warning('Error finding version number in "%s --version" stdout:\n%s',
594 settings.GIT_EXECUTABLE_PATH, output)
595 settings.GIT_EXECUTABLE_PATH, output)
595
596
596 return ver
597 return ver
General Comments 0
You need to be logged in to leave comments. Login now