##// END OF EJS Templates
ruff: code-cleanups
super-admin -
r1100:bb0b5a8e python3
parent child Browse files
Show More
@@ -1,208 +1,208 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import textwrap
22 import textwrap
23 import string
23 import string
24 import functools
24 import functools
25 import logging
25 import logging
26 import tempfile
26 import tempfile
27 import logging.config
27 import logging.config
28 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
29
29
30 # skip keys, that are set here, so we don't double process those
30 # skip keys, that are set here, so we don't double process those
31 set_keys = {
31 set_keys = {
32 '__file__': ''
32 '__file__': ''
33 }
33 }
34
34
35
35
36 def str2bool(_str):
36 def str2bool(_str):
37 """
37 """
38 returns True/False value from given string, it tries to translate the
38 returns True/False value from given string, it tries to translate the
39 string into boolean
39 string into boolean
40
40
41 :param _str: string value to translate into boolean
41 :param _str: string value to translate into boolean
42 :rtype: boolean
42 :rtype: boolean
43 :returns: boolean from given string
43 :returns: boolean from given string
44 """
44 """
45 if _str is None:
45 if _str is None:
46 return False
46 return False
47 if _str in (True, False):
47 if _str in (True, False):
48 return _str
48 return _str
49 _str = str(_str).strip().lower()
49 _str = str(_str).strip().lower()
50 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
50 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
51
51
52
52
53 def aslist(obj, sep=None, strip=True):
53 def aslist(obj, sep=None, strip=True):
54 """
54 """
55 Returns given string separated by sep as list
55 Returns given string separated by sep as list
56
56
57 :param obj:
57 :param obj:
58 :param sep:
58 :param sep:
59 :param strip:
59 :param strip:
60 """
60 """
61 if isinstance(obj, str):
61 if isinstance(obj, str):
62 if obj in ['', ""]:
62 if obj in ['', ""]:
63 return []
63 return []
64
64
65 lst = obj.split(sep)
65 lst = obj.split(sep)
66 if strip:
66 if strip:
67 lst = [v.strip() for v in lst]
67 lst = [v.strip() for v in lst]
68 return lst
68 return lst
69 elif isinstance(obj, (list, tuple)):
69 elif isinstance(obj, (list, tuple)):
70 return obj
70 return obj
71 elif obj is None:
71 elif obj is None:
72 return []
72 return []
73 else:
73 else:
74 return [obj]
74 return [obj]
75
75
76
76
77 class SettingsMaker(object):
77 class SettingsMaker(object):
78
78
79 def __init__(self, app_settings):
79 def __init__(self, app_settings):
80 self.settings = app_settings
80 self.settings = app_settings
81
81
82 @classmethod
82 @classmethod
83 def _bool_func(cls, input_val):
83 def _bool_func(cls, input_val):
84 if isinstance(input_val, bytes):
84 if isinstance(input_val, bytes):
85 # decode to str
85 # decode to str
86 input_val = input_val.decode('utf8')
86 input_val = input_val.decode('utf8')
87 return str2bool(input_val)
87 return str2bool(input_val)
88
88
89 @classmethod
89 @classmethod
90 def _int_func(cls, input_val):
90 def _int_func(cls, input_val):
91 return int(input_val)
91 return int(input_val)
92
92
93 @classmethod
93 @classmethod
94 def _list_func(cls, input_val, sep=','):
94 def _list_func(cls, input_val, sep=','):
95 return aslist(input_val, sep=sep)
95 return aslist(input_val, sep=sep)
96
96
97 @classmethod
97 @classmethod
98 def _string_func(cls, input_val, lower=True):
98 def _string_func(cls, input_val, lower=True):
99 if lower:
99 if lower:
100 input_val = input_val.lower()
100 input_val = input_val.lower()
101 return input_val
101 return input_val
102
102
103 @classmethod
103 @classmethod
104 def _float_func(cls, input_val):
104 def _float_func(cls, input_val):
105 return float(input_val)
105 return float(input_val)
106
106
107 @classmethod
107 @classmethod
108 def _dir_func(cls, input_val, ensure_dir=False, mode=0o755):
108 def _dir_func(cls, input_val, ensure_dir=False, mode=0o755):
109
109
110 # ensure we have our dir created
110 # ensure we have our dir created
111 if not os.path.isdir(input_val) and ensure_dir:
111 if not os.path.isdir(input_val) and ensure_dir:
112 os.makedirs(input_val, mode=mode)
112 os.makedirs(input_val, mode=mode)
113
113
114 if not os.path.isdir(input_val):
114 if not os.path.isdir(input_val):
115 raise Exception('Dir at {} does not exist'.format(input_val))
115 raise Exception('Dir at {} does not exist'.format(input_val))
116 return input_val
116 return input_val
117
117
118 @classmethod
118 @classmethod
119 def _file_path_func(cls, input_val, ensure_dir=False, mode=0o755):
119 def _file_path_func(cls, input_val, ensure_dir=False, mode=0o755):
120 dirname = os.path.dirname(input_val)
120 dirname = os.path.dirname(input_val)
121 cls._dir_func(dirname, ensure_dir=ensure_dir)
121 cls._dir_func(dirname, ensure_dir=ensure_dir)
122 return input_val
122 return input_val
123
123
124 @classmethod
124 @classmethod
125 def _key_transformator(cls, key):
125 def _key_transformator(cls, key):
126 return "{}_{}".format('RC'.upper(), key.upper().replace('.', '_').replace('-', '_'))
126 return "{}_{}".format('RC'.upper(), key.upper().replace('.', '_').replace('-', '_'))
127
127
128 def maybe_env_key(self, key):
128 def maybe_env_key(self, key):
129 # now maybe we have this KEY in env, search and use the value with higher priority.
129 # now maybe we have this KEY in env, search and use the value with higher priority.
130 transformed_key = self._key_transformator(key)
130 transformed_key = self._key_transformator(key)
131 envvar_value = os.environ.get(transformed_key)
131 envvar_value = os.environ.get(transformed_key)
132 if envvar_value:
132 if envvar_value:
133 log.debug('using `%s` key instead of `%s` key for config', transformed_key, key)
133 log.debug('using `%s` key instead of `%s` key for config', transformed_key, key)
134
134
135 return envvar_value
135 return envvar_value
136
136
137 def env_expand(self):
137 def env_expand(self):
138 replaced = {}
138 replaced = {}
139 for k, v in self.settings.items():
139 for k, v in self.settings.items():
140 if k not in set_keys:
140 if k not in set_keys:
141 envvar_value = self.maybe_env_key(k)
141 envvar_value = self.maybe_env_key(k)
142 if envvar_value:
142 if envvar_value:
143 replaced[k] = envvar_value
143 replaced[k] = envvar_value
144 set_keys[k] = envvar_value
144 set_keys[k] = envvar_value
145
145
146 # replace ALL keys updated
146 # replace ALL keys updated
147 self.settings.update(replaced)
147 self.settings.update(replaced)
148
148
149 def enable_logging(self, logging_conf=None, level='INFO', formatter='generic'):
149 def enable_logging(self, logging_conf=None, level='INFO', formatter='generic'):
150 """
150 """
151 Helper to enable debug on running instance
151 Helper to enable debug on running instance
152 :return:
152 :return:
153 """
153 """
154
154
155 if not str2bool(self.settings.get('logging.autoconfigure')):
155 if not str2bool(self.settings.get('logging.autoconfigure')):
156 log.info('logging configuration based on main .ini file')
156 log.info('logging configuration based on main .ini file')
157 return
157 return
158
158
159 if logging_conf is None:
159 if logging_conf is None:
160 logging_conf = self.settings.get('logging.logging_conf_file') or ''
160 logging_conf = self.settings.get('logging.logging_conf_file') or ''
161
161
162 if not os.path.isfile(logging_conf):
162 if not os.path.isfile(logging_conf):
163 log.error('Unable to setup logging based on %s, '
163 log.error('Unable to setup logging based on %s, '
164 'file does not exist.... specify path using logging.logging_conf_file= config setting. ', logging_conf)
164 'file does not exist.... specify path using logging.logging_conf_file= config setting. ', logging_conf)
165 return
165 return
166
166
167 with open(logging_conf, 'rb') as f:
167 with open(logging_conf, 'rt') as f:
168 ini_template = textwrap.dedent(f.read())
168 ini_template = textwrap.dedent(f.read())
169 ini_template = string.Template(ini_template).safe_substitute(
169 ini_template = string.Template(ini_template).safe_substitute(
170 RC_LOGGING_LEVEL=os.environ.get('RC_LOGGING_LEVEL', '') or level,
170 RC_LOGGING_LEVEL=os.environ.get('RC_LOGGING_LEVEL', '') or level,
171 RC_LOGGING_FORMATTER=os.environ.get('RC_LOGGING_FORMATTER', '') or formatter
171 RC_LOGGING_FORMATTER=os.environ.get('RC_LOGGING_FORMATTER', '') or formatter
172 )
172 )
173
173
174 with tempfile.NamedTemporaryFile(prefix='rc_logging_', suffix='.ini', delete=False) as f:
174 with tempfile.NamedTemporaryFile(prefix='rc_logging_', suffix='.ini', delete=False) as f:
175 log.info('Saved Temporary LOGGING config at %s', f.name)
175 log.info('Saved Temporary LOGGING config at %s', f.name)
176 f.write(ini_template)
176 f.write(ini_template)
177
177
178 logging.config.fileConfig(f.name)
178 logging.config.fileConfig(f.name)
179 os.remove(f.name)
179 os.remove(f.name)
180
180
181 def make_setting(self, key, default, lower=False, default_when_empty=False, parser=None):
181 def make_setting(self, key, default, lower=False, default_when_empty=False, parser=None):
182 input_val = self.settings.get(key, default)
182 input_val = self.settings.get(key, default)
183
183
184 if default_when_empty and not input_val:
184 if default_when_empty and not input_val:
185 # use default value when value is set in the config but it is empty
185 # use default value when value is set in the config but it is empty
186 input_val = default
186 input_val = default
187
187
188 parser_func = {
188 parser_func = {
189 'bool': self._bool_func,
189 'bool': self._bool_func,
190 'int': self._int_func,
190 'int': self._int_func,
191 'list': self._list_func,
191 'list': self._list_func,
192 'list:newline': functools.partial(self._list_func, sep='/n'),
192 'list:newline': functools.partial(self._list_func, sep='/n'),
193 'list:spacesep': functools.partial(self._list_func, sep=' '),
193 'list:spacesep': functools.partial(self._list_func, sep=' '),
194 'string': functools.partial(self._string_func, lower=lower),
194 'string': functools.partial(self._string_func, lower=lower),
195 'dir': self._dir_func,
195 'dir': self._dir_func,
196 'dir:ensured': functools.partial(self._dir_func, ensure_dir=True),
196 'dir:ensured': functools.partial(self._dir_func, ensure_dir=True),
197 'file': self._file_path_func,
197 'file': self._file_path_func,
198 'file:ensured': functools.partial(self._file_path_func, ensure_dir=True),
198 'file:ensured': functools.partial(self._file_path_func, ensure_dir=True),
199 None: lambda i: i
199 None: lambda i: i
200 }[parser]
200 }[parser]
201
201
202 envvar_value = self.maybe_env_key(key)
202 envvar_value = self.maybe_env_key(key)
203 if envvar_value:
203 if envvar_value:
204 input_val = envvar_value
204 input_val = envvar_value
205 set_keys[key] = input_val
205 set_keys[key] = input_val
206
206
207 self.settings[key] = parser_func(input_val)
207 self.settings[key] = parser_func(input_val)
208 return self.settings[key]
208 return self.settings[key]
@@ -1,173 +1,174 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # RhodeCode VCSServer provides access to different vcs backends via network.
3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2020 RhodeCode GmbH
4 # Copyright (C) 2014-2020 RhodeCode GmbH
5 #
5 #
6 # This program is free software; you can redistribute it and/or modify
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
9 # (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software Foundation,
17 # along with this program; if not, write to the Free Software Foundation,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
19
20
20
21 import os
21 import os
22 import time
22 import time
23 import datetime
23 import datetime
24 import msgpack
24 import msgpack
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import tempfile
27 import tempfile
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31 # NOTE: Any changes should be synced with exc_tracking at rhodecode.lib.exc_tracking
31 # NOTE: Any changes should be synced with exc_tracking at rhodecode.lib.exc_tracking
32 global_prefix = 'vcsserver'
32 global_prefix = 'vcsserver'
33 exc_store_dir_name = 'rc_exception_store_v1'
33 exc_store_dir_name = 'rc_exception_store_v1'
34
34
35
35
36 def exc_serialize(exc_id, tb, exc_type):
36 def exc_serialize(exc_id, tb, exc_type):
37
37
38 data = {
38 data = {
39 'version': 'v1',
39 'version': 'v1',
40 'exc_id': exc_id,
40 'exc_id': exc_id,
41 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
41 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
42 'exc_timestamp': repr(time.time()),
42 'exc_timestamp': repr(time.time()),
43 'exc_message': tb,
43 'exc_message': tb,
44 'exc_type': exc_type,
44 'exc_type': exc_type,
45 }
45 }
46 return msgpack.packb(data), data
46 return msgpack.packb(data), data
47
47
48
48
49 def exc_unserialize(tb):
49 def exc_unserialize(tb):
50 return msgpack.unpackb(tb)
50 return msgpack.unpackb(tb)
51
51
52
52
53 def get_exc_store():
53 def get_exc_store():
54 """
54 """
55 Get and create exception store if it's not existing
55 Get and create exception store if it's not existing
56 """
56 """
57 import vcsserver as app
57 import vcsserver as app
58
58
59 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
59 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
60 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
60 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
61
61
62 _exc_store_path = os.path.abspath(_exc_store_path)
62 _exc_store_path = os.path.abspath(_exc_store_path)
63 if not os.path.isdir(_exc_store_path):
63 if not os.path.isdir(_exc_store_path):
64 os.makedirs(_exc_store_path)
64 os.makedirs(_exc_store_path)
65 log.debug('Initializing exceptions store at %s', _exc_store_path)
65 log.debug('Initializing exceptions store at %s', _exc_store_path)
66 return _exc_store_path
66 return _exc_store_path
67
67
68
68
69 def _store_exception(exc_id, exc_info, prefix, request_path=''):
69 def _store_exception(exc_id, exc_info, prefix, request_path=''):
70 exc_type, exc_value, exc_traceback = exc_info
70 exc_type, exc_value, exc_traceback = exc_info
71
71
72 tb = ''.join(traceback.format_exception(
72 tb = ''.join(traceback.format_exception(
73 exc_type, exc_value, exc_traceback, None))
73 exc_type, exc_value, exc_traceback, None))
74
74
75 detailed_tb = getattr(exc_value, '_org_exc_tb', None)
75 detailed_tb = getattr(exc_value, '_org_exc_tb', None)
76
76
77 if detailed_tb:
77 if detailed_tb:
78 remote_tb = detailed_tb
78 if isinstance(detailed_tb, str):
79 if isinstance(detailed_tb, str):
79 remote_tb = [detailed_tb]
80 remote_tb = [detailed_tb]
80
81
81 tb += (
82 tb += (
82 '\n+++ BEG SOURCE EXCEPTION +++\n\n'
83 '\n+++ BEG SOURCE EXCEPTION +++\n\n'
83 '{}\n'
84 '{}\n'
84 '+++ END SOURCE EXCEPTION +++\n'
85 '+++ END SOURCE EXCEPTION +++\n'
85 ''.format('\n'.join(remote_tb))
86 ''.format('\n'.join(remote_tb))
86 )
87 )
87
88
88 # Avoid that remote_tb also appears in the frame
89 # Avoid that remote_tb also appears in the frame
89 del remote_tb
90 del remote_tb
90
91
91 exc_type_name = exc_type.__name__
92 exc_type_name = exc_type.__name__
92 exc_store_path = get_exc_store()
93 exc_store_path = get_exc_store()
93 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name)
94 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name)
94 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
95 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
95 if not os.path.isdir(exc_store_path):
96 if not os.path.isdir(exc_store_path):
96 os.makedirs(exc_store_path)
97 os.makedirs(exc_store_path)
97 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
98 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
98 with open(stored_exc_path, 'wb') as f:
99 with open(stored_exc_path, 'wb') as f:
99 f.write(exc_data)
100 f.write(exc_data)
100 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
101 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
101
102
102 log.error(
103 log.error(
103 'error occurred handling this request.\n'
104 'error occurred handling this request.\n'
104 'Path: `%s`, tb: %s',
105 'Path: `%s`, tb: %s',
105 request_path, tb)
106 request_path, tb)
106
107
107
108
108 def store_exception(exc_id, exc_info, prefix=global_prefix, request_path=''):
109 def store_exception(exc_id, exc_info, prefix=global_prefix, request_path=''):
109 """
110 """
110 Example usage::
111 Example usage::
111
112
112 exc_info = sys.exc_info()
113 exc_info = sys.exc_info()
113 store_exception(id(exc_info), exc_info)
114 store_exception(id(exc_info), exc_info)
114 """
115 """
115
116
116 try:
117 try:
117 _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix,
118 _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix,
118 request_path=request_path)
119 request_path=request_path)
119 except Exception:
120 except Exception:
120 log.exception('Failed to store exception `%s` information', exc_id)
121 log.exception('Failed to store exception `%s` information', exc_id)
121 # there's no way this can fail, it will crash server badly if it does.
122 # there's no way this can fail, it will crash server badly if it does.
122 pass
123 pass
123
124
124
125
125 def _find_exc_file(exc_id, prefix=global_prefix):
126 def _find_exc_file(exc_id, prefix=global_prefix):
126 exc_store_path = get_exc_store()
127 exc_store_path = get_exc_store()
127 if prefix:
128 if prefix:
128 exc_id = '{}_{}'.format(exc_id, prefix)
129 exc_id = '{}_{}'.format(exc_id, prefix)
129 else:
130 else:
130 # search without a prefix
131 # search without a prefix
131 exc_id = '{}'.format(exc_id)
132 exc_id = '{}'.format(exc_id)
132
133
133 # we need to search the store for such start pattern as above
134 # we need to search the store for such start pattern as above
134 for fname in os.listdir(exc_store_path):
135 for fname in os.listdir(exc_store_path):
135 if fname.startswith(exc_id):
136 if fname.startswith(exc_id):
136 exc_id = os.path.join(exc_store_path, fname)
137 exc_id = os.path.join(exc_store_path, fname)
137 break
138 break
138 continue
139 continue
139 else:
140 else:
140 exc_id = None
141 exc_id = None
141
142
142 return exc_id
143 return exc_id
143
144
144
145
145 def _read_exception(exc_id, prefix):
146 def _read_exception(exc_id, prefix):
146 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
147 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
147 if exc_id_file_path:
148 if exc_id_file_path:
148 with open(exc_id_file_path, 'rb') as f:
149 with open(exc_id_file_path, 'rb') as f:
149 return exc_unserialize(f.read())
150 return exc_unserialize(f.read())
150 else:
151 else:
151 log.debug('Exception File `%s` not found', exc_id_file_path)
152 log.debug('Exception File `%s` not found', exc_id_file_path)
152 return None
153 return None
153
154
154
155
155 def read_exception(exc_id, prefix=global_prefix):
156 def read_exception(exc_id, prefix=global_prefix):
156 try:
157 try:
157 return _read_exception(exc_id=exc_id, prefix=prefix)
158 return _read_exception(exc_id=exc_id, prefix=prefix)
158 except Exception:
159 except Exception:
159 log.exception('Failed to read exception `%s` information', exc_id)
160 log.exception('Failed to read exception `%s` information', exc_id)
160 # there's no way this can fail, it will crash server badly if it does.
161 # there's no way this can fail, it will crash server badly if it does.
161 return None
162 return None
162
163
163
164
164 def delete_exception(exc_id, prefix=global_prefix):
165 def delete_exception(exc_id, prefix=global_prefix):
165 try:
166 try:
166 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
167 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
167 if exc_id_file_path:
168 if exc_id_file_path:
168 os.remove(exc_id_file_path)
169 os.remove(exc_id_file_path)
169
170
170 except Exception:
171 except Exception:
171 log.exception('Failed to remove exception `%s` information', exc_id)
172 log.exception('Failed to remove exception `%s` information', exc_id)
172 # there's no way this can fail, it will crash server badly if it does.
173 # there's no way this can fail, it will crash server badly if it does.
173 pass
174 pass
@@ -1,80 +1,86 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 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 General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import logging
18 import logging
19
19 from dogpile.cache import register_backend
20 from dogpile.cache import register_backend
21
20 module_name = 'vcsserver'
22 module_name = 'vcsserver'
21
23
22 register_backend(
24 register_backend(
23 "dogpile.cache.rc.memory_lru", f"{module_name}.lib.rc_cache.backends",
25 "dogpile.cache.rc.memory_lru", f"{module_name}.lib.rc_cache.backends",
24 "LRUMemoryBackend")
26 "LRUMemoryBackend")
25
27
26 register_backend(
28 register_backend(
27 "dogpile.cache.rc.file_namespace", f"{module_name}.lib.rc_cache.backends",
29 "dogpile.cache.rc.file_namespace", f"{module_name}.lib.rc_cache.backends",
28 "FileNamespaceBackend")
30 "FileNamespaceBackend")
29
31
30 register_backend(
32 register_backend(
31 "dogpile.cache.rc.redis", f"{module_name}.lib.rc_cache.backends",
33 "dogpile.cache.rc.redis", f"{module_name}.lib.rc_cache.backends",
32 "RedisPickleBackend")
34 "RedisPickleBackend")
33
35
34 register_backend(
36 register_backend(
35 "dogpile.cache.rc.redis_msgpack", f"{module_name}.lib.rc_cache.backends",
37 "dogpile.cache.rc.redis_msgpack", f"{module_name}.lib.rc_cache.backends",
36 "RedisMsgPackBackend")
38 "RedisMsgPackBackend")
37
39
38
40
39 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
40
42
41 from . import region_meta
43 from . import region_meta
42 from .utils import (
44 from .utils import (
43 get_default_cache_settings, backend_key_generator, get_or_create_region,
45 backend_key_generator,
44 clear_cache_namespace, make_region)
46 clear_cache_namespace,
47 get_default_cache_settings,
48 get_or_create_region,
49 make_region,
50 )
45
51
46
52
47 def configure_dogpile_cache(settings):
53 def configure_dogpile_cache(settings):
48 cache_dir = settings.get('cache_dir')
54 cache_dir = settings.get('cache_dir')
49 if cache_dir:
55 if cache_dir:
50 region_meta.dogpile_config_defaults['cache_dir'] = cache_dir
56 region_meta.dogpile_config_defaults['cache_dir'] = cache_dir
51
57
52 rc_cache_data = get_default_cache_settings(settings, prefixes=['rc_cache.'])
58 rc_cache_data = get_default_cache_settings(settings, prefixes=['rc_cache.'])
53
59
54 # inspect available namespaces
60 # inspect available namespaces
55 avail_regions = set()
61 avail_regions = set()
56 for key in rc_cache_data.keys():
62 for key in rc_cache_data.keys():
57 namespace_name = key.split('.', 1)[0]
63 namespace_name = key.split('.', 1)[0]
58 if namespace_name in avail_regions:
64 if namespace_name in avail_regions:
59 continue
65 continue
60
66
61 avail_regions.add(namespace_name)
67 avail_regions.add(namespace_name)
62 log.debug('dogpile: found following cache regions: %s', namespace_name)
68 log.debug('dogpile: found following cache regions: %s', namespace_name)
63
69
64 new_region = make_region(
70 new_region = make_region(
65 name=namespace_name,
71 name=namespace_name,
66 function_key_generator=None
72 function_key_generator=None
67 )
73 )
68
74
69 new_region.configure_from_config(settings, 'rc_cache.{}.'.format(namespace_name))
75 new_region.configure_from_config(settings, 'rc_cache.{}.'.format(namespace_name))
70 new_region.function_key_generator = backend_key_generator(new_region.actual_backend)
76 new_region.function_key_generator = backend_key_generator(new_region.actual_backend)
71 if log.isEnabledFor(logging.DEBUG):
77 if log.isEnabledFor(logging.DEBUG):
72 region_args = dict(backend=new_region.actual_backend.__class__,
78 region_args = dict(backend=new_region.actual_backend.__class__,
73 region_invalidator=new_region.region_invalidator.__class__)
79 region_invalidator=new_region.region_invalidator.__class__)
74 log.debug('dogpile: registering a new region `%s` %s', namespace_name, region_args)
80 log.debug('dogpile: registering a new region `%s` %s', namespace_name, region_args)
75
81
76 region_meta.dogpile_cache_regions[namespace_name] = new_region
82 region_meta.dogpile_cache_regions[namespace_name] = new_region
77
83
78
84
79 def includeme(config):
85 def includeme(config):
80 configure_dogpile_cache(config.registry.settings)
86 configure_dogpile_cache(config.registry.settings)
@@ -1,239 +1,238 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 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 General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import errno
19 import fcntl
20 import functools
21 import logging
22 import pickle
18 import time
23 import time
19 import errno
20 import logging
21 import functools
22
24
23 import msgpack
25 import msgpack
24 import redis
26 import redis
25 import pickle
27
26 import fcntl
27 flock_org = fcntl.flock
28 flock_org = fcntl.flock
28 from typing import Union
29 from typing import Union
29
30
31 from dogpile.cache.api import Deserializer, Serializer
32 from dogpile.cache.backends import file as file_backend
30 from dogpile.cache.backends import memory as memory_backend
33 from dogpile.cache.backends import memory as memory_backend
31 from dogpile.cache.backends import file as file_backend
32 from dogpile.cache.backends import redis as redis_backend
34 from dogpile.cache.backends import redis as redis_backend
33 from dogpile.cache.backends.file import FileLock
35 from dogpile.cache.backends.file import FileLock
34 from dogpile.cache.util import memoized_property
36 from dogpile.cache.util import memoized_property
35 from dogpile.cache.api import Serializer, Deserializer
36
37 from pyramid.settings import asbool
37 from pyramid.settings import asbool
38
38
39 from vcsserver.lib.memory_lru_dict import LRUDict, LRUDictDebug
39 from vcsserver.lib.memory_lru_dict import LRUDict, LRUDictDebug
40 from vcsserver.str_utils import safe_str, safe_bytes
40 from vcsserver.str_utils import safe_bytes, safe_str
41
42
41
43 _default_max_size = 1024
42 _default_max_size = 1024
44
43
45 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
46
45
47
46
48 class LRUMemoryBackend(memory_backend.MemoryBackend):
47 class LRUMemoryBackend(memory_backend.MemoryBackend):
49 key_prefix = 'lru_mem_backend'
48 key_prefix = 'lru_mem_backend'
50 pickle_values = False
49 pickle_values = False
51
50
52 def __init__(self, arguments):
51 def __init__(self, arguments):
53 max_size = arguments.pop('max_size', _default_max_size)
52 max_size = arguments.pop('max_size', _default_max_size)
54
53
55 LRUDictClass = LRUDict
54 LRUDictClass = LRUDict
56 if arguments.pop('log_key_count', None):
55 if arguments.pop('log_key_count', None):
57 LRUDictClass = LRUDictDebug
56 LRUDictClass = LRUDictDebug
58
57
59 arguments['cache_dict'] = LRUDictClass(max_size)
58 arguments['cache_dict'] = LRUDictClass(max_size)
60 super(LRUMemoryBackend, self).__init__(arguments)
59 super(LRUMemoryBackend, self).__init__(arguments)
61
60
62 def delete(self, key):
61 def delete(self, key):
63 try:
62 try:
64 del self._cache[key]
63 del self._cache[key]
65 except KeyError:
64 except KeyError:
66 # we don't care if key isn't there at deletion
65 # we don't care if key isn't there at deletion
67 pass
66 pass
68
67
69 def delete_multi(self, keys):
68 def delete_multi(self, keys):
70 for key in keys:
69 for key in keys:
71 self.delete(key)
70 self.delete(key)
72
71
73
72
74 class PickleSerializer:
73 class PickleSerializer:
75 serializer: Union[None, Serializer] = staticmethod( # type: ignore
74 serializer: Union[None, Serializer] = staticmethod( # type: ignore
76 functools.partial(pickle.dumps, protocol=pickle.HIGHEST_PROTOCOL)
75 functools.partial(pickle.dumps, protocol=pickle.HIGHEST_PROTOCOL)
77 )
76 )
78 deserializer: Union[None, Deserializer] = staticmethod( # type: ignore
77 deserializer: Union[None, Deserializer] = staticmethod( # type: ignore
79 functools.partial(pickle.loads)
78 functools.partial(pickle.loads)
80 )
79 )
81
80
82
81
83 class MsgPackSerializer(object):
82 class MsgPackSerializer(object):
84 serializer: Union[None, Serializer] = staticmethod( # type: ignore
83 serializer: Union[None, Serializer] = staticmethod( # type: ignore
85 msgpack.packb
84 msgpack.packb
86 )
85 )
87 deserializer: Union[None, Deserializer] = staticmethod( # type: ignore
86 deserializer: Union[None, Deserializer] = staticmethod( # type: ignore
88 functools.partial(msgpack.unpackb, use_list=False)
87 functools.partial(msgpack.unpackb, use_list=False)
89 )
88 )
90
89
91
90
92 class CustomLockFactory(FileLock):
91 class CustomLockFactory(FileLock):
93
92
94 pass
93 pass
95
94
96
95
97 class FileNamespaceBackend(PickleSerializer, file_backend.DBMBackend):
96 class FileNamespaceBackend(PickleSerializer, file_backend.DBMBackend):
98 key_prefix = 'file_backend'
97 key_prefix = 'file_backend'
99
98
100 def __init__(self, arguments):
99 def __init__(self, arguments):
101 arguments['lock_factory'] = CustomLockFactory
100 arguments['lock_factory'] = CustomLockFactory
102 db_file = arguments.get('filename')
101 db_file = arguments.get('filename')
103
102
104 log.debug('initialing %s DB in %s', self.__class__.__name__, db_file)
103 log.debug('initialing %s DB in %s', self.__class__.__name__, db_file)
105 try:
104 try:
106 super(FileNamespaceBackend, self).__init__(arguments)
105 super(FileNamespaceBackend, self).__init__(arguments)
107 except Exception:
106 except Exception:
108 log.exception('Failed to initialize db at: %s', db_file)
107 log.exception('Failed to initialize db at: %s', db_file)
109 raise
108 raise
110
109
111 def __repr__(self):
110 def __repr__(self):
112 return '{} `{}`'.format(self.__class__, self.filename)
111 return '{} `{}`'.format(self.__class__, self.filename)
113
112
114 def list_keys(self, prefix: bytes = b''):
113 def list_keys(self, prefix: bytes = b''):
115 prefix = b'%b:%b' % (safe_bytes(self.key_prefix), safe_bytes(prefix))
114 prefix = b'%b:%b' % (safe_bytes(self.key_prefix), safe_bytes(prefix))
116
115
117 def cond(dbm_key: bytes):
116 def cond(dbm_key: bytes):
118 if not prefix:
117 if not prefix:
119 return True
118 return True
120
119
121 if dbm_key.startswith(prefix):
120 if dbm_key.startswith(prefix):
122 return True
121 return True
123 return False
122 return False
124
123
125 with self._dbm_file(True) as dbm:
124 with self._dbm_file(True) as dbm:
126 try:
125 try:
127 return list(filter(cond, dbm.keys()))
126 return list(filter(cond, dbm.keys()))
128 except Exception:
127 except Exception:
129 log.error('Failed to fetch DBM keys from DB: %s', self.get_store())
128 log.error('Failed to fetch DBM keys from DB: %s', self.get_store())
130 raise
129 raise
131
130
132 def get_store(self):
131 def get_store(self):
133 return self.filename
132 return self.filename
134
133
135
134
136 class BaseRedisBackend(redis_backend.RedisBackend):
135 class BaseRedisBackend(redis_backend.RedisBackend):
137 key_prefix = ''
136 key_prefix = ''
138
137
139 def __init__(self, arguments):
138 def __init__(self, arguments):
140 super(BaseRedisBackend, self).__init__(arguments)
139 super(BaseRedisBackend, self).__init__(arguments)
141 self._lock_timeout = self.lock_timeout
140 self._lock_timeout = self.lock_timeout
142 self._lock_auto_renewal = asbool(arguments.pop("lock_auto_renewal", True))
141 self._lock_auto_renewal = asbool(arguments.pop("lock_auto_renewal", True))
143
142
144 if self._lock_auto_renewal and not self._lock_timeout:
143 if self._lock_auto_renewal and not self._lock_timeout:
145 # set default timeout for auto_renewal
144 # set default timeout for auto_renewal
146 self._lock_timeout = 30
145 self._lock_timeout = 30
147
146
148 def _create_client(self):
147 def _create_client(self):
149 args = {}
148 args = {}
150
149
151 if self.url is not None:
150 if self.url is not None:
152 args.update(url=self.url)
151 args.update(url=self.url)
153
152
154 else:
153 else:
155 args.update(
154 args.update(
156 host=self.host, password=self.password,
155 host=self.host, password=self.password,
157 port=self.port, db=self.db
156 port=self.port, db=self.db
158 )
157 )
159
158
160 connection_pool = redis.ConnectionPool(**args)
159 connection_pool = redis.ConnectionPool(**args)
161 self.writer_client = redis.StrictRedis(
160 self.writer_client = redis.StrictRedis(
162 connection_pool=connection_pool
161 connection_pool=connection_pool
163 )
162 )
164 self.reader_client = self.writer_client
163 self.reader_client = self.writer_client
165
164
166 def list_keys(self, prefix=''):
165 def list_keys(self, prefix=''):
167 prefix = '{}:{}*'.format(self.key_prefix, prefix)
166 prefix = '{}:{}*'.format(self.key_prefix, prefix)
168 return self.reader_client.keys(prefix)
167 return self.reader_client.keys(prefix)
169
168
170 def get_store(self):
169 def get_store(self):
171 return self.reader_client.connection_pool
170 return self.reader_client.connection_pool
172
171
173 def get_mutex(self, key):
172 def get_mutex(self, key):
174 if self.distributed_lock:
173 if self.distributed_lock:
175 lock_key = '_lock_{0}'.format(safe_str(key))
174 lock_key = '_lock_{0}'.format(safe_str(key))
176 return get_mutex_lock(
175 return get_mutex_lock(
177 self.writer_client, lock_key,
176 self.writer_client, lock_key,
178 self._lock_timeout,
177 self._lock_timeout,
179 auto_renewal=self._lock_auto_renewal
178 auto_renewal=self._lock_auto_renewal
180 )
179 )
181 else:
180 else:
182 return None
181 return None
183
182
184
183
185 class RedisPickleBackend(PickleSerializer, BaseRedisBackend):
184 class RedisPickleBackend(PickleSerializer, BaseRedisBackend):
186 key_prefix = 'redis_pickle_backend'
185 key_prefix = 'redis_pickle_backend'
187 pass
186 pass
188
187
189
188
190 class RedisMsgPackBackend(MsgPackSerializer, BaseRedisBackend):
189 class RedisMsgPackBackend(MsgPackSerializer, BaseRedisBackend):
191 key_prefix = 'redis_msgpack_backend'
190 key_prefix = 'redis_msgpack_backend'
192 pass
191 pass
193
192
194
193
195 def get_mutex_lock(client, lock_key, lock_timeout, auto_renewal=False):
194 def get_mutex_lock(client, lock_key, lock_timeout, auto_renewal=False):
196 from vcsserver.lib._vendor import redis_lock
195 from vcsserver.lib._vendor import redis_lock
197
196
198 class _RedisLockWrapper(object):
197 class _RedisLockWrapper(object):
199 """LockWrapper for redis_lock"""
198 """LockWrapper for redis_lock"""
200
199
201 @classmethod
200 @classmethod
202 def get_lock(cls):
201 def get_lock(cls):
203 return redis_lock.Lock(
202 return redis_lock.Lock(
204 redis_client=client,
203 redis_client=client,
205 name=lock_key,
204 name=lock_key,
206 expire=lock_timeout,
205 expire=lock_timeout,
207 auto_renewal=auto_renewal,
206 auto_renewal=auto_renewal,
208 strict=True,
207 strict=True,
209 )
208 )
210
209
211 def __repr__(self):
210 def __repr__(self):
212 return "{}:{}".format(self.__class__.__name__, lock_key)
211 return "{}:{}".format(self.__class__.__name__, lock_key)
213
212
214 def __str__(self):
213 def __str__(self):
215 return "{}:{}".format(self.__class__.__name__, lock_key)
214 return "{}:{}".format(self.__class__.__name__, lock_key)
216
215
217 def __init__(self):
216 def __init__(self):
218 self.lock = self.get_lock()
217 self.lock = self.get_lock()
219 self.lock_key = lock_key
218 self.lock_key = lock_key
220
219
221 def acquire(self, wait=True):
220 def acquire(self, wait=True):
222 log.debug('Trying to acquire Redis lock for key %s', self.lock_key)
221 log.debug('Trying to acquire Redis lock for key %s', self.lock_key)
223 try:
222 try:
224 acquired = self.lock.acquire(wait)
223 acquired = self.lock.acquire(wait)
225 log.debug('Got lock for key %s, %s', self.lock_key, acquired)
224 log.debug('Got lock for key %s, %s', self.lock_key, acquired)
226 return acquired
225 return acquired
227 except redis_lock.AlreadyAcquired:
226 except redis_lock.AlreadyAcquired:
228 return False
227 return False
229 except redis_lock.AlreadyStarted:
228 except redis_lock.AlreadyStarted:
230 # refresh thread exists, but it also means we acquired the lock
229 # refresh thread exists, but it also means we acquired the lock
231 return True
230 return True
232
231
233 def release(self):
232 def release(self):
234 try:
233 try:
235 self.lock.release()
234 self.lock.release()
236 except redis_lock.NotAcquired:
235 except redis_lock.NotAcquired:
237 pass
236 pass
238
237
239 return _RedisLockWrapper()
238 return _RedisLockWrapper()
@@ -1,207 +1,207 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 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 General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import functools
19 import logging
18 import os
20 import os
19 import time
21 import time
20 import logging
22
21 import functools
22 import decorator
23 import decorator
23
24 from dogpile.cache import CacheRegion
24 from dogpile.cache import CacheRegion
25
25
26 from vcsserver.lib.rc_cache import region_meta
26 from vcsserver.str_utils import safe_bytes
27 from vcsserver.str_utils import safe_bytes
27 from vcsserver.utils import sha1
28 from vcsserver.utils import sha1
28 from vcsserver.lib.rc_cache import region_meta
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class RhodeCodeCacheRegion(CacheRegion):
33 class RhodeCodeCacheRegion(CacheRegion):
34
34
35 def conditional_cache_on_arguments(
35 def conditional_cache_on_arguments(
36 self, namespace=None,
36 self, namespace=None,
37 expiration_time=None,
37 expiration_time=None,
38 should_cache_fn=None,
38 should_cache_fn=None,
39 to_str=str,
39 to_str=str,
40 function_key_generator=None,
40 function_key_generator=None,
41 condition=True):
41 condition=True):
42 """
42 """
43 Custom conditional decorator, that will not touch any dogpile internals if
43 Custom conditional decorator, that will not touch any dogpile internals if
44 condition isn't meet. This works a bit different than should_cache_fn
44 condition isn't meet. This works a bit different from should_cache_fn
45 And it's faster in cases we don't ever want to compute cached values
45 And it's faster in cases we don't ever want to compute cached values
46 """
46 """
47 expiration_time_is_callable = callable(expiration_time)
47 expiration_time_is_callable = callable(expiration_time)
48
48
49 if function_key_generator is None:
49 if function_key_generator is None:
50 function_key_generator = self.function_key_generator
50 function_key_generator = self.function_key_generator
51
51
52 def get_or_create_for_user_func(key_generator, user_func, *arg, **kw):
52 def get_or_create_for_user_func(key_generator, user_func, *arg, **kw):
53
53
54 if not condition:
54 if not condition:
55 log.debug('Calling un-cached method:%s', user_func.__name__)
55 log.debug('Calling un-cached method:%s', user_func.__name__)
56 start = time.time()
56 start = time.time()
57 result = user_func(*arg, **kw)
57 result = user_func(*arg, **kw)
58 total = time.time() - start
58 total = time.time() - start
59 log.debug('un-cached method:%s took %.4fs', user_func.__name__, total)
59 log.debug('un-cached method:%s took %.4fs', user_func.__name__, total)
60 return result
60 return result
61
61
62 key = key_generator(*arg, **kw)
62 key = key_generator(*arg, **kw)
63
63
64 timeout = expiration_time() if expiration_time_is_callable \
64 timeout = expiration_time() if expiration_time_is_callable \
65 else expiration_time
65 else expiration_time
66
66
67 log.debug('Calling cached method:`%s`', user_func.__name__)
67 log.debug('Calling cached method:`%s`', user_func.__name__)
68 return self.get_or_create(key, user_func, timeout, should_cache_fn, (arg, kw))
68 return self.get_or_create(key, user_func, timeout, should_cache_fn, (arg, kw))
69
69
70 def cache_decorator(user_func):
70 def cache_decorator(user_func):
71 if to_str is str:
71 if to_str is str:
72 # backwards compatible
72 # backwards compatible
73 key_generator = function_key_generator(namespace, user_func)
73 key_generator = function_key_generator(namespace, user_func)
74 else:
74 else:
75 key_generator = function_key_generator(namespace, user_func, to_str=to_str)
75 key_generator = function_key_generator(namespace, user_func, to_str=to_str)
76
76
77 def refresh(*arg, **kw):
77 def refresh(*arg, **kw):
78 """
78 """
79 Like invalidate, but regenerates the value instead
79 Like invalidate, but regenerates the value instead
80 """
80 """
81 key = key_generator(*arg, **kw)
81 key = key_generator(*arg, **kw)
82 value = user_func(*arg, **kw)
82 value = user_func(*arg, **kw)
83 self.set(key, value)
83 self.set(key, value)
84 return value
84 return value
85
85
86 def invalidate(*arg, **kw):
86 def invalidate(*arg, **kw):
87 key = key_generator(*arg, **kw)
87 key = key_generator(*arg, **kw)
88 self.delete(key)
88 self.delete(key)
89
89
90 def set_(value, *arg, **kw):
90 def set_(value, *arg, **kw):
91 key = key_generator(*arg, **kw)
91 key = key_generator(*arg, **kw)
92 self.set(key, value)
92 self.set(key, value)
93
93
94 def get(*arg, **kw):
94 def get(*arg, **kw):
95 key = key_generator(*arg, **kw)
95 key = key_generator(*arg, **kw)
96 return self.get(key)
96 return self.get(key)
97
97
98 user_func.set = set_
98 user_func.set = set_
99 user_func.invalidate = invalidate
99 user_func.invalidate = invalidate
100 user_func.get = get
100 user_func.get = get
101 user_func.refresh = refresh
101 user_func.refresh = refresh
102 user_func.key_generator = key_generator
102 user_func.key_generator = key_generator
103 user_func.original = user_func
103 user_func.original = user_func
104
104
105 # Use `decorate` to preserve the signature of :param:`user_func`.
105 # Use `decorate` to preserve the signature of :param:`user_func`.
106 return decorator.decorate(user_func, functools.partial(
106 return decorator.decorate(user_func, functools.partial(
107 get_or_create_for_user_func, key_generator))
107 get_or_create_for_user_func, key_generator))
108
108
109 return cache_decorator
109 return cache_decorator
110
110
111
111
112 def make_region(*arg, **kw):
112 def make_region(*arg, **kw):
113 return RhodeCodeCacheRegion(*arg, **kw)
113 return RhodeCodeCacheRegion(*arg, **kw)
114
114
115
115
116 def get_default_cache_settings(settings, prefixes=None):
116 def get_default_cache_settings(settings, prefixes=None):
117 prefixes = prefixes or []
117 prefixes = prefixes or []
118 cache_settings = {}
118 cache_settings = {}
119 for key in settings.keys():
119 for key in settings.keys():
120 for prefix in prefixes:
120 for prefix in prefixes:
121 if key.startswith(prefix):
121 if key.startswith(prefix):
122 name = key.split(prefix)[1].strip()
122 name = key.split(prefix)[1].strip()
123 val = settings[key]
123 val = settings[key]
124 if isinstance(val, str):
124 if isinstance(val, str):
125 val = val.strip()
125 val = val.strip()
126 cache_settings[name] = val
126 cache_settings[name] = val
127 return cache_settings
127 return cache_settings
128
128
129
129
130 def compute_key_from_params(*args):
130 def compute_key_from_params(*args):
131 """
131 """
132 Helper to compute key from given params to be used in cache manager
132 Helper to compute key from given params to be used in cache manager
133 """
133 """
134 return sha1(safe_bytes("_".join(map(str, args))))
134 return sha1(safe_bytes("_".join(map(str, args))))
135
135
136
136
137 def backend_key_generator(backend):
137 def backend_key_generator(backend):
138 """
138 """
139 Special wrapper that also sends over the backend to the key generator
139 Special wrapper that also sends over the backend to the key generator
140 """
140 """
141 def wrapper(namespace, fn):
141 def wrapper(namespace, fn):
142 return key_generator(backend, namespace, fn)
142 return key_generator(backend, namespace, fn)
143 return wrapper
143 return wrapper
144
144
145
145
146 def key_generator(backend, namespace, fn):
146 def key_generator(backend, namespace, fn):
147 fname = fn.__name__
147 fname = fn.__name__
148
148
149 def generate_key(*args):
149 def generate_key(*args):
150 backend_prefix = getattr(backend, 'key_prefix', None) or 'backend_prefix'
150 backend_prefix = getattr(backend, 'key_prefix', None) or 'backend_prefix'
151 namespace_pref = namespace or 'default_namespace'
151 namespace_pref = namespace or 'default_namespace'
152 arg_key = compute_key_from_params(*args)
152 arg_key = compute_key_from_params(*args)
153 final_key = "{}:{}:{}_{}".format(backend_prefix, namespace_pref, fname, arg_key)
153 final_key = f"{backend_prefix}:{namespace_pref}:{fname}_{arg_key}"
154
154
155 return final_key
155 return final_key
156
156
157 return generate_key
157 return generate_key
158
158
159
159
160 def get_or_create_region(region_name, region_namespace=None):
160 def get_or_create_region(region_name, region_namespace=None):
161 from vcsserver.lib.rc_cache.backends import FileNamespaceBackend
161 from vcsserver.lib.rc_cache.backends import FileNamespaceBackend
162 region_obj = region_meta.dogpile_cache_regions.get(region_name)
162 region_obj = region_meta.dogpile_cache_regions.get(region_name)
163 if not region_obj:
163 if not region_obj:
164 reg_keys = list(region_meta.dogpile_cache_regions.keys())
164 reg_keys = list(region_meta.dogpile_cache_regions.keys())
165 raise EnvironmentError(f'Region `{region_name}` not in configured: {reg_keys}.')
165 raise EnvironmentError(f'Region `{region_name}` not in configured: {reg_keys}.')
166
166
167 region_uid_name = f'{region_name}:{region_namespace}'
167 region_uid_name = f'{region_name}:{region_namespace}'
168 if isinstance(region_obj.actual_backend, FileNamespaceBackend):
168 if isinstance(region_obj.actual_backend, FileNamespaceBackend):
169 region_exist = region_meta.dogpile_cache_regions.get(region_namespace)
169 region_exist = region_meta.dogpile_cache_regions.get(region_namespace)
170 if region_exist:
170 if region_exist:
171 log.debug('Using already configured region: %s', region_namespace)
171 log.debug('Using already configured region: %s', region_namespace)
172 return region_exist
172 return region_exist
173 cache_dir = region_meta.dogpile_config_defaults['cache_dir']
173 cache_dir = region_meta.dogpile_config_defaults['cache_dir']
174 expiration_time = region_obj.expiration_time
174 expiration_time = region_obj.expiration_time
175
175
176 if not os.path.isdir(cache_dir):
176 if not os.path.isdir(cache_dir):
177 os.makedirs(cache_dir)
177 os.makedirs(cache_dir)
178 new_region = make_region(
178 new_region = make_region(
179 name=region_uid_name,
179 name=region_uid_name,
180 function_key_generator=backend_key_generator(region_obj.actual_backend)
180 function_key_generator=backend_key_generator(region_obj.actual_backend)
181 )
181 )
182 namespace_filename = os.path.join(
182 namespace_filename = os.path.join(
183 cache_dir, f"{region_namespace}.cache.dbm")
183 cache_dir, f"{region_namespace}.cache.dbm")
184 # special type that allows 1db per namespace
184 # special type that allows 1db per namespace
185 new_region.configure(
185 new_region.configure(
186 backend='dogpile.cache.rc.file_namespace',
186 backend='dogpile.cache.rc.file_namespace',
187 expiration_time=expiration_time,
187 expiration_time=expiration_time,
188 arguments={"filename": namespace_filename}
188 arguments={"filename": namespace_filename}
189 )
189 )
190
190
191 # create and save in region caches
191 # create and save in region caches
192 log.debug('configuring new region: %s', region_uid_name)
192 log.debug('configuring new region: %s', region_uid_name)
193 region_obj = region_meta.dogpile_cache_regions[region_namespace] = new_region
193 region_obj = region_meta.dogpile_cache_regions[region_namespace] = new_region
194
194
195 return region_obj
195 return region_obj
196
196
197
197
198 def clear_cache_namespace(cache_region, cache_namespace_uid, invalidate=False):
198 def clear_cache_namespace(cache_region, cache_namespace_uid, invalidate=False):
199 region = get_or_create_region(cache_region, cache_namespace_uid)
199 region = get_or_create_region(cache_region, cache_namespace_uid)
200 cache_keys = region.backend.list_keys(prefix=cache_namespace_uid)
200 cache_keys = region.backend.list_keys(prefix=cache_namespace_uid)
201 num_delete_keys = len(cache_keys)
201 num_delete_keys = len(cache_keys)
202 if invalidate:
202 if invalidate:
203 region.invalidate(hard=False)
203 region.invalidate(hard=False)
204 else:
204 else:
205 if num_delete_keys:
205 if num_delete_keys:
206 region.delete_multi(cache_keys)
206 region.delete_multi(cache_keys)
207 return num_delete_keys
207 return num_delete_keys
@@ -1,53 +1,70 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
1 from vcsserver.lib._vendor.statsd import client_from_config
18 from vcsserver.lib._vendor.statsd import client_from_config
2
19
3
20
4 class StatsdClientNotInitialised(Exception):
21 class StatsdClientNotInitialised(Exception):
5 pass
22 pass
6
23
7
24
8 class _Singleton(type):
25 class _Singleton(type):
9 """A metaclass that creates a Singleton base class when called."""
26 """A metaclass that creates a Singleton base class when called."""
10
27
11 _instances = {}
28 _instances = {}
12
29
13 def __call__(cls, *args, **kwargs):
30 def __call__(cls, *args, **kwargs):
14 if cls not in cls._instances:
31 if cls not in cls._instances:
15 cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs)
32 cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs)
16 return cls._instances[cls]
33 return cls._instances[cls]
17
34
18
35
19 class Singleton(_Singleton("SingletonMeta", (object,), {})):
36 class Singleton(_Singleton("SingletonMeta", (object,), {})):
20 pass
37 pass
21
38
22
39
23 class StatsdClientClass(Singleton):
40 class StatsdClientClass(Singleton):
24 setup_run = False
41 setup_run = False
25 statsd_client = None
42 statsd_client = None
26 statsd = None
43 statsd = None
27 strict_mode_init = False
44 strict_mode_init = False
28
45
29 def __getattribute__(self, name):
46 def __getattribute__(self, name):
30
47
31 if name.startswith("statsd"):
48 if name.startswith("statsd"):
32 if self.setup_run:
49 if self.setup_run:
33 return super(StatsdClientClass, self).__getattribute__(name)
50 return super(StatsdClientClass, self).__getattribute__(name)
34 else:
51 else:
35 if self.strict_mode_init:
52 if self.strict_mode_init:
36 raise StatsdClientNotInitialised(f"requested key was {name}")
53 raise StatsdClientNotInitialised(f"requested key was {name}")
37 return None
54 return None
38
55
39 return super(StatsdClientClass, self).__getattribute__(name)
56 return super(StatsdClientClass, self).__getattribute__(name)
40
57
41 def setup(self, settings):
58 def setup(self, settings):
42 """
59 """
43 Initialize the client
60 Initialize the client
44 """
61 """
45 strict_init_mode = settings.pop('statsd_strict_init', False)
62 strict_init_mode = settings.pop('statsd_strict_init', False)
46
63
47 statsd = client_from_config(settings)
64 statsd = client_from_config(settings)
48 self.statsd = statsd
65 self.statsd = statsd
49 self.statsd_client = statsd
66 self.statsd_client = statsd
50 self.setup_run = True
67 self.setup_run = True
51
68
52
69
53 StatsdClient = StatsdClientClass()
70 StatsdClient = StatsdClientClass()
@@ -1,1368 +1,1371 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 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 General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import collections
18 import collections
19 import logging
19 import logging
20 import os
20 import os
21 import posixpath as vcspath
21 import posixpath as vcspath
22 import re
22 import re
23 import stat
23 import stat
24 import traceback
24 import traceback
25 import urllib.request, urllib.parse, urllib.error
25 import urllib.request, urllib.parse, urllib.error
26 import urllib.request, urllib.error, urllib.parse
26 import urllib.request, urllib.error, urllib.parse
27 from functools import wraps
27 from functools import wraps
28
28
29 import more_itertools
29 import more_itertools
30 import pygit2
30 import pygit2
31 from pygit2 import Repository as LibGit2Repo
31 from pygit2 import Repository as LibGit2Repo
32 from pygit2 import index as LibGit2Index
32 from pygit2 import index as LibGit2Index
33 from dulwich import index, objects
33 from dulwich import index, objects
34 from dulwich.client import HttpGitClient, LocalGitClient
34 from dulwich.client import HttpGitClient, LocalGitClient
35 from dulwich.errors import (
35 from dulwich.errors import (
36 NotGitRepository, ChecksumMismatch, WrongObjectException,
36 NotGitRepository, ChecksumMismatch, WrongObjectException,
37 MissingCommitError, ObjectMissing, HangupException,
37 MissingCommitError, ObjectMissing, HangupException,
38 UnexpectedCommandError)
38 UnexpectedCommandError)
39 from dulwich.repo import Repo as DulwichRepo
39 from dulwich.repo import Repo as DulwichRepo
40 from dulwich.server import update_server_info
40 from dulwich.server import update_server_info
41
41
42 from vcsserver import exceptions, settings, subprocessio
42 from vcsserver import exceptions, settings, subprocessio
43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes, ascii_str, ascii_bytes
43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes, ascii_str, ascii_bytes
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, archive_repo, BinaryEnvelope
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, archive_repo, BinaryEnvelope
45 from vcsserver.hgcompat import (
45 from vcsserver.hgcompat import (
46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
47 from vcsserver.git_lfs.lib import LFSOidStore
47 from vcsserver.git_lfs.lib import LFSOidStore
48 from vcsserver.vcs_base import RemoteBase
48 from vcsserver.vcs_base import RemoteBase
49
49
50 DIR_STAT = stat.S_IFDIR
50 DIR_STAT = stat.S_IFDIR
51 FILE_MODE = stat.S_IFMT
51 FILE_MODE = stat.S_IFMT
52 GIT_LINK = objects.S_IFGITLINK
52 GIT_LINK = objects.S_IFGITLINK
53 PEELED_REF_MARKER = b'^{}'
53 PEELED_REF_MARKER = b'^{}'
54 HEAD_MARKER = b'HEAD'
54 HEAD_MARKER = b'HEAD'
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 def reraise_safe_exceptions(func):
59 def reraise_safe_exceptions(func):
60 """Converts Dulwich exceptions to something neutral."""
60 """Converts Dulwich exceptions to something neutral."""
61
61
62 @wraps(func)
62 @wraps(func)
63 def wrapper(*args, **kwargs):
63 def wrapper(*args, **kwargs):
64 try:
64 try:
65 return func(*args, **kwargs)
65 return func(*args, **kwargs)
66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
67 exc = exceptions.LookupException(org_exc=e)
67 exc = exceptions.LookupException(org_exc=e)
68 raise exc(safe_str(e))
68 raise exc(safe_str(e))
69 except (HangupException, UnexpectedCommandError) as e:
69 except (HangupException, UnexpectedCommandError) as e:
70 exc = exceptions.VcsException(org_exc=e)
70 exc = exceptions.VcsException(org_exc=e)
71 raise exc(safe_str(e))
71 raise exc(safe_str(e))
72 except Exception as e:
72 except Exception as e:
73 # NOTE(marcink): becuase of how dulwich handles some exceptions
73 # NOTE(marcink): because of how dulwich handles some exceptions
74 # (KeyError on empty repos), we cannot track this and catch all
74 # (KeyError on empty repos), we cannot track this and catch all
75 # exceptions, it's an exceptions from other handlers
75 # exceptions, it's an exceptions from other handlers
76 #if not hasattr(e, '_vcs_kind'):
76 #if not hasattr(e, '_vcs_kind'):
77 #log.exception("Unhandled exception in git remote call")
77 #log.exception("Unhandled exception in git remote call")
78 #raise_from_original(exceptions.UnhandledException)
78 #raise_from_original(exceptions.UnhandledException)
79 raise
79 raise
80 return wrapper
80 return wrapper
81
81
82
82
83 class Repo(DulwichRepo):
83 class Repo(DulwichRepo):
84 """
84 """
85 A wrapper for dulwich Repo class.
85 A wrapper for dulwich Repo class.
86
86
87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
88 "Too many open files" error. We need to close all opened file descriptors
88 "Too many open files" error. We need to close all opened file descriptors
89 once the repo object is destroyed.
89 once the repo object is destroyed.
90 """
90 """
91 def __del__(self):
91 def __del__(self):
92 if hasattr(self, 'object_store'):
92 if hasattr(self, 'object_store'):
93 self.close()
93 self.close()
94
94
95
95
96 class Repository(LibGit2Repo):
96 class Repository(LibGit2Repo):
97
97
98 def __enter__(self):
98 def __enter__(self):
99 return self
99 return self
100
100
101 def __exit__(self, exc_type, exc_val, exc_tb):
101 def __exit__(self, exc_type, exc_val, exc_tb):
102 self.free()
102 self.free()
103
103
104
104
105 class GitFactory(RepoFactory):
105 class GitFactory(RepoFactory):
106 repo_type = 'git'
106 repo_type = 'git'
107
107
108 def _create_repo(self, wire, create, use_libgit2=False):
108 def _create_repo(self, wire, create, use_libgit2=False):
109 if use_libgit2:
109 if use_libgit2:
110 return Repository(safe_bytes(wire['path']))
110 return Repository(safe_bytes(wire['path']))
111 else:
111 else:
112 # dulwich mode
112 # dulwich mode
113 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
113 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
114 return Repo(repo_path)
114 return Repo(repo_path)
115
115
116 def repo(self, wire, create=False, use_libgit2=False):
116 def repo(self, wire, create=False, use_libgit2=False):
117 """
117 """
118 Get a repository instance for the given path.
118 Get a repository instance for the given path.
119 """
119 """
120 return self._create_repo(wire, create, use_libgit2)
120 return self._create_repo(wire, create, use_libgit2)
121
121
122 def repo_libgit2(self, wire):
122 def repo_libgit2(self, wire):
123 return self.repo(wire, use_libgit2=True)
123 return self.repo(wire, use_libgit2=True)
124
124
125
125
126 class GitRemote(RemoteBase):
126 class GitRemote(RemoteBase):
127
127
128 def __init__(self, factory):
128 def __init__(self, factory):
129 self._factory = factory
129 self._factory = factory
130 self._bulk_methods = {
130 self._bulk_methods = {
131 "date": self.date,
131 "date": self.date,
132 "author": self.author,
132 "author": self.author,
133 "branch": self.branch,
133 "branch": self.branch,
134 "message": self.message,
134 "message": self.message,
135 "parents": self.parents,
135 "parents": self.parents,
136 "_commit": self.revision,
136 "_commit": self.revision,
137 }
137 }
138
138
139 def _wire_to_config(self, wire):
139 def _wire_to_config(self, wire):
140 if 'config' in wire:
140 if 'config' in wire:
141 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
141 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
142 return {}
142 return {}
143
143
144 def _remote_conf(self, config):
144 def _remote_conf(self, config):
145 params = [
145 params = [
146 '-c', 'core.askpass=""',
146 '-c', 'core.askpass=""',
147 ]
147 ]
148 ssl_cert_dir = config.get('vcs_ssl_dir')
148 ssl_cert_dir = config.get('vcs_ssl_dir')
149 if ssl_cert_dir:
149 if ssl_cert_dir:
150 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
150 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
151 return params
151 return params
152
152
153 @reraise_safe_exceptions
153 @reraise_safe_exceptions
154 def discover_git_version(self):
154 def discover_git_version(self):
155 stdout, _ = self.run_git_command(
155 stdout, _ = self.run_git_command(
156 {}, ['--version'], _bare=True, _safe=True)
156 {}, ['--version'], _bare=True, _safe=True)
157 prefix = b'git version'
157 prefix = b'git version'
158 if stdout.startswith(prefix):
158 if stdout.startswith(prefix):
159 stdout = stdout[len(prefix):]
159 stdout = stdout[len(prefix):]
160 return safe_str(stdout.strip())
160 return safe_str(stdout.strip())
161
161
162 @reraise_safe_exceptions
162 @reraise_safe_exceptions
163 def is_empty(self, wire):
163 def is_empty(self, wire):
164 repo_init = self._factory.repo_libgit2(wire)
164 repo_init = self._factory.repo_libgit2(wire)
165 with repo_init as repo:
165 with repo_init as repo:
166
166
167 try:
167 try:
168 has_head = repo.head.name
168 has_head = repo.head.name
169 if has_head:
169 if has_head:
170 return False
170 return False
171
171
172 # NOTE(marcink): check again using more expensive method
172 # NOTE(marcink): check again using more expensive method
173 return repo.is_empty
173 return repo.is_empty
174 except Exception:
174 except Exception:
175 pass
175 pass
176
176
177 return True
177 return True
178
178
179 @reraise_safe_exceptions
179 @reraise_safe_exceptions
180 def assert_correct_path(self, wire):
180 def assert_correct_path(self, wire):
181 cache_on, context_uid, repo_id = self._cache_on(wire)
181 cache_on, context_uid, repo_id = self._cache_on(wire)
182 region = self._region(wire)
182 region = self._region(wire)
183
183
184 @region.conditional_cache_on_arguments(condition=cache_on)
184 @region.conditional_cache_on_arguments(condition=cache_on)
185 def _assert_correct_path(_context_uid, _repo_id):
185 def _assert_correct_path(_context_uid, _repo_id):
186 try:
186 try:
187 repo_init = self._factory.repo_libgit2(wire)
187 repo_init = self._factory.repo_libgit2(wire)
188 with repo_init as repo:
188 with repo_init as repo:
189 pass
189 pass
190 except pygit2.GitError:
190 except pygit2.GitError:
191 path = wire.get('path')
191 path = wire.get('path')
192 tb = traceback.format_exc()
192 tb = traceback.format_exc()
193 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
193 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
194 return False
194 return False
195
195
196 return True
196 return True
197 return _assert_correct_path(context_uid, repo_id)
197 return _assert_correct_path(context_uid, repo_id)
198
198
199 @reraise_safe_exceptions
199 @reraise_safe_exceptions
200 def bare(self, wire):
200 def bare(self, wire):
201 repo_init = self._factory.repo_libgit2(wire)
201 repo_init = self._factory.repo_libgit2(wire)
202 with repo_init as repo:
202 with repo_init as repo:
203 return repo.is_bare
203 return repo.is_bare
204
204
205 @reraise_safe_exceptions
205 @reraise_safe_exceptions
206 def blob_as_pretty_string(self, wire, sha):
206 def blob_as_pretty_string(self, wire, sha):
207 repo_init = self._factory.repo_libgit2(wire)
207 repo_init = self._factory.repo_libgit2(wire)
208 with repo_init as repo:
208 with repo_init as repo:
209 blob_obj = repo[sha]
209 blob_obj = repo[sha]
210 return BinaryEnvelope(blob_obj.data)
210 return BinaryEnvelope(blob_obj.data)
211
211
212 @reraise_safe_exceptions
212 @reraise_safe_exceptions
213 def blob_raw_length(self, wire, sha):
213 def blob_raw_length(self, wire, sha):
214 cache_on, context_uid, repo_id = self._cache_on(wire)
214 cache_on, context_uid, repo_id = self._cache_on(wire)
215 region = self._region(wire)
215 region = self._region(wire)
216
216
217 @region.conditional_cache_on_arguments(condition=cache_on)
217 @region.conditional_cache_on_arguments(condition=cache_on)
218 def _blob_raw_length(_repo_id, _sha):
218 def _blob_raw_length(_repo_id, _sha):
219
219
220 repo_init = self._factory.repo_libgit2(wire)
220 repo_init = self._factory.repo_libgit2(wire)
221 with repo_init as repo:
221 with repo_init as repo:
222 blob = repo[sha]
222 blob = repo[sha]
223 return blob.size
223 return blob.size
224
224
225 return _blob_raw_length(repo_id, sha)
225 return _blob_raw_length(repo_id, sha)
226
226
227 def _parse_lfs_pointer(self, raw_content):
227 def _parse_lfs_pointer(self, raw_content):
228 spec_string = b'version https://git-lfs.github.com/spec'
228 spec_string = b'version https://git-lfs.github.com/spec'
229 if raw_content and raw_content.startswith(spec_string):
229 if raw_content and raw_content.startswith(spec_string):
230
230
231 pattern = re.compile(rb"""
231 pattern = re.compile(rb"""
232 (?:\n)?
232 (?:\n)?
233 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
233 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
234 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
234 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
235 ^size[ ](?P<oid_size>[0-9]+)\n
235 ^size[ ](?P<oid_size>[0-9]+)\n
236 (?:\n)?
236 (?:\n)?
237 """, re.VERBOSE | re.MULTILINE)
237 """, re.VERBOSE | re.MULTILINE)
238 match = pattern.match(raw_content)
238 match = pattern.match(raw_content)
239 if match:
239 if match:
240 return match.groupdict()
240 return match.groupdict()
241
241
242 return {}
242 return {}
243
243
244 @reraise_safe_exceptions
244 @reraise_safe_exceptions
245 def is_large_file(self, wire, commit_id):
245 def is_large_file(self, wire, commit_id):
246 cache_on, context_uid, repo_id = self._cache_on(wire)
246 cache_on, context_uid, repo_id = self._cache_on(wire)
247 region = self._region(wire)
247 region = self._region(wire)
248
248
249 @region.conditional_cache_on_arguments(condition=cache_on)
249 @region.conditional_cache_on_arguments(condition=cache_on)
250 def _is_large_file(_repo_id, _sha):
250 def _is_large_file(_repo_id, _sha):
251 repo_init = self._factory.repo_libgit2(wire)
251 repo_init = self._factory.repo_libgit2(wire)
252 with repo_init as repo:
252 with repo_init as repo:
253 blob = repo[commit_id]
253 blob = repo[commit_id]
254 if blob.is_binary:
254 if blob.is_binary:
255 return {}
255 return {}
256
256
257 return self._parse_lfs_pointer(blob.data)
257 return self._parse_lfs_pointer(blob.data)
258
258
259 return _is_large_file(repo_id, commit_id)
259 return _is_large_file(repo_id, commit_id)
260
260
261 @reraise_safe_exceptions
261 @reraise_safe_exceptions
262 def is_binary(self, wire, tree_id):
262 def is_binary(self, wire, tree_id):
263 cache_on, context_uid, repo_id = self._cache_on(wire)
263 cache_on, context_uid, repo_id = self._cache_on(wire)
264 region = self._region(wire)
264 region = self._region(wire)
265
265
266 @region.conditional_cache_on_arguments(condition=cache_on)
266 @region.conditional_cache_on_arguments(condition=cache_on)
267 def _is_binary(_repo_id, _tree_id):
267 def _is_binary(_repo_id, _tree_id):
268 repo_init = self._factory.repo_libgit2(wire)
268 repo_init = self._factory.repo_libgit2(wire)
269 with repo_init as repo:
269 with repo_init as repo:
270 blob_obj = repo[tree_id]
270 blob_obj = repo[tree_id]
271 return blob_obj.is_binary
271 return blob_obj.is_binary
272
272
273 return _is_binary(repo_id, tree_id)
273 return _is_binary(repo_id, tree_id)
274
274
275 @reraise_safe_exceptions
275 @reraise_safe_exceptions
276 def md5_hash(self, wire, tree_id):
276 def md5_hash(self, wire, tree_id):
277 cache_on, context_uid, repo_id = self._cache_on(wire)
277 cache_on, context_uid, repo_id = self._cache_on(wire)
278 region = self._region(wire)
278 region = self._region(wire)
279
279
280 @region.conditional_cache_on_arguments(condition=cache_on)
280 @region.conditional_cache_on_arguments(condition=cache_on)
281 def _md5_hash(_repo_id, _tree_id):
281 def _md5_hash(_repo_id, _tree_id):
282 return ''
282 return ''
283
283
284 return _md5_hash(repo_id, tree_id)
284 return _md5_hash(repo_id, tree_id)
285
285
286 @reraise_safe_exceptions
286 @reraise_safe_exceptions
287 def in_largefiles_store(self, wire, oid):
287 def in_largefiles_store(self, wire, oid):
288 conf = self._wire_to_config(wire)
288 conf = self._wire_to_config(wire)
289 repo_init = self._factory.repo_libgit2(wire)
289 repo_init = self._factory.repo_libgit2(wire)
290 with repo_init as repo:
290 with repo_init as repo:
291 repo_name = repo.path
291 repo_name = repo.path
292
292
293 store_location = conf.get('vcs_git_lfs_store_location')
293 store_location = conf.get('vcs_git_lfs_store_location')
294 if store_location:
294 if store_location:
295
295
296 store = LFSOidStore(
296 store = LFSOidStore(
297 oid=oid, repo=repo_name, store_location=store_location)
297 oid=oid, repo=repo_name, store_location=store_location)
298 return store.has_oid()
298 return store.has_oid()
299
299
300 return False
300 return False
301
301
302 @reraise_safe_exceptions
302 @reraise_safe_exceptions
303 def store_path(self, wire, oid):
303 def store_path(self, wire, oid):
304 conf = self._wire_to_config(wire)
304 conf = self._wire_to_config(wire)
305 repo_init = self._factory.repo_libgit2(wire)
305 repo_init = self._factory.repo_libgit2(wire)
306 with repo_init as repo:
306 with repo_init as repo:
307 repo_name = repo.path
307 repo_name = repo.path
308
308
309 store_location = conf.get('vcs_git_lfs_store_location')
309 store_location = conf.get('vcs_git_lfs_store_location')
310 if store_location:
310 if store_location:
311 store = LFSOidStore(
311 store = LFSOidStore(
312 oid=oid, repo=repo_name, store_location=store_location)
312 oid=oid, repo=repo_name, store_location=store_location)
313 return store.oid_path
313 return store.oid_path
314 raise ValueError('Unable to fetch oid with path {}'.format(oid))
314 raise ValueError('Unable to fetch oid with path {}'.format(oid))
315
315
316 @reraise_safe_exceptions
316 @reraise_safe_exceptions
317 def bulk_request(self, wire, rev, pre_load):
317 def bulk_request(self, wire, rev, pre_load):
318 cache_on, context_uid, repo_id = self._cache_on(wire)
318 cache_on, context_uid, repo_id = self._cache_on(wire)
319 region = self._region(wire)
319 region = self._region(wire)
320
320
321 @region.conditional_cache_on_arguments(condition=cache_on)
321 @region.conditional_cache_on_arguments(condition=cache_on)
322 def _bulk_request(_repo_id, _rev, _pre_load):
322 def _bulk_request(_repo_id, _rev, _pre_load):
323 result = {}
323 result = {}
324 for attr in pre_load:
324 for attr in pre_load:
325 try:
325 try:
326 method = self._bulk_methods[attr]
326 method = self._bulk_methods[attr]
327 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
327 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
328 args = [wire, rev]
328 args = [wire, rev]
329 result[attr] = method(*args)
329 result[attr] = method(*args)
330 except KeyError as e:
330 except KeyError as e:
331 raise exceptions.VcsException(e)(f"Unknown bulk attribute: {attr}")
331 raise exceptions.VcsException(e)(f"Unknown bulk attribute: {attr}")
332 return result
332 return result
333
333
334 return _bulk_request(repo_id, rev, sorted(pre_load))
334 return _bulk_request(repo_id, rev, sorted(pre_load))
335
335
336 def _build_opener(self, url):
336 def _build_opener(self, url):
337 handlers = []
337 handlers = []
338 url_obj = url_parser(url)
338 url_obj = url_parser(url)
339 _, authinfo = url_obj.authinfo()
339 _, authinfo = url_obj.authinfo()
340
340
341 if authinfo:
341 if authinfo:
342 # create a password manager
342 # create a password manager
343 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
343 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
344 passmgr.add_password(*authinfo)
344 passmgr.add_password(*authinfo)
345
345
346 handlers.extend((httpbasicauthhandler(passmgr),
346 handlers.extend((httpbasicauthhandler(passmgr),
347 httpdigestauthhandler(passmgr)))
347 httpdigestauthhandler(passmgr)))
348
348
349 return urllib.request.build_opener(*handlers)
349 return urllib.request.build_opener(*handlers)
350
350
351 def _type_id_to_name(self, type_id: int):
351 def _type_id_to_name(self, type_id: int):
352 return {
352 return {
353 1: 'commit',
353 1: 'commit',
354 2: 'tree',
354 2: 'tree',
355 3: 'blob',
355 3: 'blob',
356 4: 'tag'
356 4: 'tag'
357 }[type_id]
357 }[type_id]
358
358
359 @reraise_safe_exceptions
359 @reraise_safe_exceptions
360 def check_url(self, url, config):
360 def check_url(self, url, config):
361 url_obj = url_parser(safe_bytes(url))
361 url_obj = url_parser(safe_bytes(url))
362 test_uri, _ = url_obj.authinfo()
362 test_uri, _ = url_obj.authinfo()
363 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
363 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
364 url_obj.query = obfuscate_qs(url_obj.query)
364 url_obj.query = obfuscate_qs(url_obj.query)
365 cleaned_uri = str(url_obj)
365 cleaned_uri = str(url_obj)
366 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
366 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
367
367
368 if not test_uri.endswith('info/refs'):
368 if not test_uri.endswith('info/refs'):
369 test_uri = test_uri.rstrip('/') + '/info/refs'
369 test_uri = test_uri.rstrip('/') + '/info/refs'
370
370
371 o = self._build_opener(url)
371 o = self._build_opener(url)
372 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
372 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
373
373
374 q = {"service": 'git-upload-pack'}
374 q = {"service": 'git-upload-pack'}
375 qs = '?%s' % urllib.parse.urlencode(q)
375 qs = '?%s' % urllib.parse.urlencode(q)
376 cu = "%s%s" % (test_uri, qs)
376 cu = "%s%s" % (test_uri, qs)
377 req = urllib.request.Request(cu, None, {})
377 req = urllib.request.Request(cu, None, {})
378
378
379 try:
379 try:
380 log.debug("Trying to open URL %s", cleaned_uri)
380 log.debug("Trying to open URL %s", cleaned_uri)
381 resp = o.open(req)
381 resp = o.open(req)
382 if resp.code != 200:
382 if resp.code != 200:
383 raise exceptions.URLError()('Return Code is not 200')
383 raise exceptions.URLError()('Return Code is not 200')
384 except Exception as e:
384 except Exception as e:
385 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
385 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
386 # means it cannot be cloned
386 # means it cannot be cloned
387 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
387 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
388
388
389 # now detect if it's proper git repo
389 # now detect if it's proper git repo
390 gitdata = resp.read()
390 gitdata = resp.read()
391 if 'service=git-upload-pack' in gitdata:
391 if 'service=git-upload-pack' in gitdata:
392 pass
392 pass
393 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
393 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
394 # old style git can return some other format !
394 # old style git can return some other format !
395 pass
395 pass
396 else:
396 else:
397 raise exceptions.URLError()(
397 raise exceptions.URLError()(
398 "url [%s] does not look like an git" % (cleaned_uri,))
398 "url [%s] does not look like an git" % (cleaned_uri,))
399
399
400 return True
400 return True
401
401
402 @reraise_safe_exceptions
402 @reraise_safe_exceptions
403 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
403 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
404 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
404 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
405 remote_refs = self.pull(wire, url, apply_refs=False)
405 remote_refs = self.pull(wire, url, apply_refs=False)
406 repo = self._factory.repo(wire)
406 repo = self._factory.repo(wire)
407 if isinstance(valid_refs, list):
407 if isinstance(valid_refs, list):
408 valid_refs = tuple(valid_refs)
408 valid_refs = tuple(valid_refs)
409
409
410 for k in remote_refs:
410 for k in remote_refs:
411 # only parse heads/tags and skip so called deferred tags
411 # only parse heads/tags and skip so called deferred tags
412 if k.startswith(valid_refs) and not k.endswith(deferred):
412 if k.startswith(valid_refs) and not k.endswith(deferred):
413 repo[k] = remote_refs[k]
413 repo[k] = remote_refs[k]
414
414
415 if update_after_clone:
415 if update_after_clone:
416 # we want to checkout HEAD
416 # we want to checkout HEAD
417 repo["HEAD"] = remote_refs["HEAD"]
417 repo["HEAD"] = remote_refs["HEAD"]
418 index.build_index_from_tree(repo.path, repo.index_path(),
418 index.build_index_from_tree(repo.path, repo.index_path(),
419 repo.object_store, repo["HEAD"].tree)
419 repo.object_store, repo["HEAD"].tree)
420
420
421 @reraise_safe_exceptions
421 @reraise_safe_exceptions
422 def branch(self, wire, commit_id):
422 def branch(self, wire, commit_id):
423 cache_on, context_uid, repo_id = self._cache_on(wire)
423 cache_on, context_uid, repo_id = self._cache_on(wire)
424 region = self._region(wire)
424 region = self._region(wire)
425
425 @region.conditional_cache_on_arguments(condition=cache_on)
426 @region.conditional_cache_on_arguments(condition=cache_on)
426 def _branch(_context_uid, _repo_id, _commit_id):
427 def _branch(_context_uid, _repo_id, _commit_id):
427 regex = re.compile('^refs/heads')
428 regex = re.compile('^refs/heads')
428
429
429 def filter_with(ref):
430 def filter_with(ref):
430 return regex.match(ref[0]) and ref[1] == _commit_id
431 return regex.match(ref[0]) and ref[1] == _commit_id
431
432
432 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
433 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
433 return [x[0].split('refs/heads/')[-1] for x in branches]
434 return [x[0].split('refs/heads/')[-1] for x in branches]
434
435
435 return _branch(context_uid, repo_id, commit_id)
436 return _branch(context_uid, repo_id, commit_id)
436
437
437 @reraise_safe_exceptions
438 @reraise_safe_exceptions
438 def commit_branches(self, wire, commit_id):
439 def commit_branches(self, wire, commit_id):
439 cache_on, context_uid, repo_id = self._cache_on(wire)
440 cache_on, context_uid, repo_id = self._cache_on(wire)
440 region = self._region(wire)
441 region = self._region(wire)
442
441 @region.conditional_cache_on_arguments(condition=cache_on)
443 @region.conditional_cache_on_arguments(condition=cache_on)
442 def _commit_branches(_context_uid, _repo_id, _commit_id):
444 def _commit_branches(_context_uid, _repo_id, _commit_id):
443 repo_init = self._factory.repo_libgit2(wire)
445 repo_init = self._factory.repo_libgit2(wire)
444 with repo_init as repo:
446 with repo_init as repo:
445 branches = [x for x in repo.branches.with_commit(_commit_id)]
447 branches = [x for x in repo.branches.with_commit(_commit_id)]
446 return branches
448 return branches
447
449
448 return _commit_branches(context_uid, repo_id, commit_id)
450 return _commit_branches(context_uid, repo_id, commit_id)
449
451
450 @reraise_safe_exceptions
452 @reraise_safe_exceptions
451 def add_object(self, wire, content):
453 def add_object(self, wire, content):
452 repo_init = self._factory.repo_libgit2(wire)
454 repo_init = self._factory.repo_libgit2(wire)
453 with repo_init as repo:
455 with repo_init as repo:
454 blob = objects.Blob()
456 blob = objects.Blob()
455 blob.set_raw_string(content)
457 blob.set_raw_string(content)
456 repo.object_store.add_object(blob)
458 repo.object_store.add_object(blob)
457 return blob.id
459 return blob.id
458
460
459 # TODO: this is quite complex, check if that can be simplified
461 # TODO: this is quite complex, check if that can be simplified
460 @reraise_safe_exceptions
462 @reraise_safe_exceptions
461 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
463 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
462 # Defines the root tree
464 # Defines the root tree
463 class _Root(object):
465 class _Root(object):
464 def __repr__(self):
466 def __repr__(self):
465 return 'ROOT TREE'
467 return 'ROOT TREE'
466 ROOT = _Root()
468 ROOT = _Root()
467
469
468 repo = self._factory.repo(wire)
470 repo = self._factory.repo(wire)
469 object_store = repo.object_store
471 object_store = repo.object_store
470
472
471 # Create tree and populates it with blobs
473 # Create tree and populates it with blobs
472 if commit_tree:
474 if commit_tree:
473 commit_tree = safe_bytes(commit_tree)
475 commit_tree = safe_bytes(commit_tree)
474
476
475 if commit_tree and repo[commit_tree]:
477 if commit_tree and repo[commit_tree]:
476 git_commit = repo[safe_bytes(commit_data['parents'][0])]
478 git_commit = repo[safe_bytes(commit_data['parents'][0])]
477 commit_tree = repo[git_commit.tree] # root tree
479 commit_tree = repo[git_commit.tree] # root tree
478 else:
480 else:
479 commit_tree = objects.Tree()
481 commit_tree = objects.Tree()
480
482
481 for node in updated:
483 for node in updated:
482 # Compute subdirs if needed
484 # Compute subdirs if needed
483 dirpath, nodename = vcspath.split(node['path'])
485 dirpath, nodename = vcspath.split(node['path'])
484 dirnames = list(map(safe_str, dirpath and dirpath.split('/') or []))
486 dirnames = list(map(safe_str, dirpath and dirpath.split('/') or []))
485 parent = commit_tree
487 parent = commit_tree
486 ancestors = [('', parent)]
488 ancestors = [('', parent)]
487
489
488 # Tries to dig for the deepest existing tree
490 # Tries to dig for the deepest existing tree
489 while dirnames:
491 while dirnames:
490 curdir = dirnames.pop(0)
492 curdir = dirnames.pop(0)
491 try:
493 try:
492 dir_id = parent[curdir][1]
494 dir_id = parent[curdir][1]
493 except KeyError:
495 except KeyError:
494 # put curdir back into dirnames and stops
496 # put curdir back into dirnames and stops
495 dirnames.insert(0, curdir)
497 dirnames.insert(0, curdir)
496 break
498 break
497 else:
499 else:
498 # If found, updates parent
500 # If found, updates parent
499 parent = repo[dir_id]
501 parent = repo[dir_id]
500 ancestors.append((curdir, parent))
502 ancestors.append((curdir, parent))
501 # Now parent is deepest existing tree and we need to create
503 # Now parent is deepest existing tree and we need to create
502 # subtrees for dirnames (in reverse order)
504 # subtrees for dirnames (in reverse order)
503 # [this only applies for nodes from added]
505 # [this only applies for nodes from added]
504 new_trees = []
506 new_trees = []
505
507
506 blob = objects.Blob.from_string(node['content'])
508 blob = objects.Blob.from_string(node['content'])
507
509
508 node_path = safe_bytes(node['node_path'])
510 node_path = safe_bytes(node['node_path'])
509
511
510 if dirnames:
512 if dirnames:
511 # If there are trees which should be created we need to build
513 # If there are trees which should be created we need to build
512 # them now (in reverse order)
514 # them now (in reverse order)
513 reversed_dirnames = list(reversed(dirnames))
515 reversed_dirnames = list(reversed(dirnames))
514 curtree = objects.Tree()
516 curtree = objects.Tree()
515 curtree[node_path] = node['mode'], blob.id
517 curtree[node_path] = node['mode'], blob.id
516 new_trees.append(curtree)
518 new_trees.append(curtree)
517 for dirname in reversed_dirnames[:-1]:
519 for dirname in reversed_dirnames[:-1]:
518 newtree = objects.Tree()
520 newtree = objects.Tree()
519 newtree[dirname] = (DIR_STAT, curtree.id)
521 newtree[dirname] = (DIR_STAT, curtree.id)
520 new_trees.append(newtree)
522 new_trees.append(newtree)
521 curtree = newtree
523 curtree = newtree
522 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
524 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
523 else:
525 else:
524 parent.add(name=node_path, mode=node['mode'], hexsha=blob.id)
526 parent.add(name=node_path, mode=node['mode'], hexsha=blob.id)
525
527
526 new_trees.append(parent)
528 new_trees.append(parent)
527 # Update ancestors
529 # Update ancestors
528 reversed_ancestors = reversed(
530 reversed_ancestors = reversed(
529 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
531 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
530 for parent, tree, path in reversed_ancestors:
532 for parent, tree, path in reversed_ancestors:
531 parent[path] = (DIR_STAT, tree.id)
533 parent[path] = (DIR_STAT, tree.id)
532 object_store.add_object(tree)
534 object_store.add_object(tree)
533
535
534 object_store.add_object(blob)
536 object_store.add_object(blob)
535 for tree in new_trees:
537 for tree in new_trees:
536 object_store.add_object(tree)
538 object_store.add_object(tree)
537
539
538 for node_path in removed:
540 for node_path in removed:
539 paths = node_path.split('/')
541 paths = node_path.split('/')
540 tree = commit_tree # start with top-level
542 tree = commit_tree # start with top-level
541 trees = [{'tree': tree, 'path': ROOT}]
543 trees = [{'tree': tree, 'path': ROOT}]
542 # Traverse deep into the forest...
544 # Traverse deep into the forest...
543 # resolve final tree by iterating the path.
545 # resolve final tree by iterating the path.
544 # e.g a/b/c.txt will get
546 # e.g a/b/c.txt will get
545 # - root as tree then
547 # - root as tree then
546 # - 'a' as tree,
548 # - 'a' as tree,
547 # - 'b' as tree,
549 # - 'b' as tree,
548 # - stop at c as blob.
550 # - stop at c as blob.
549 for path in paths:
551 for path in paths:
550 try:
552 try:
551 obj = repo[tree[path][1]]
553 obj = repo[tree[path][1]]
552 if isinstance(obj, objects.Tree):
554 if isinstance(obj, objects.Tree):
553 trees.append({'tree': obj, 'path': path})
555 trees.append({'tree': obj, 'path': path})
554 tree = obj
556 tree = obj
555 except KeyError:
557 except KeyError:
556 break
558 break
557 #PROBLEM:
559 #PROBLEM:
558 """
560 """
559 We're not editing same reference tree object
561 We're not editing same reference tree object
560 """
562 """
561 # Cut down the blob and all rotten trees on the way back...
563 # Cut down the blob and all rotten trees on the way back...
562 for path, tree_data in reversed(list(zip(paths, trees))):
564 for path, tree_data in reversed(list(zip(paths, trees))):
563 tree = tree_data['tree']
565 tree = tree_data['tree']
564 tree.__delitem__(path)
566 tree.__delitem__(path)
565 # This operation edits the tree, we need to mark new commit back
567 # This operation edits the tree, we need to mark new commit back
566
568
567 if len(tree) > 0:
569 if len(tree) > 0:
568 # This tree still has elements - don't remove it or any
570 # This tree still has elements - don't remove it or any
569 # of it's parents
571 # of it's parents
570 break
572 break
571
573
572 object_store.add_object(commit_tree)
574 object_store.add_object(commit_tree)
573
575
574 # Create commit
576 # Create commit
575 commit = objects.Commit()
577 commit = objects.Commit()
576 commit.tree = commit_tree.id
578 commit.tree = commit_tree.id
577 bytes_keys = [
579 bytes_keys = [
578 'author',
580 'author',
579 'committer',
581 'committer',
580 'message',
582 'message',
581 'encoding',
583 'encoding',
582 'parents'
584 'parents'
583 ]
585 ]
584
586
585 for k, v in commit_data.items():
587 for k, v in commit_data.items():
586 if k in bytes_keys:
588 if k in bytes_keys:
587 if k == 'parents':
589 if k == 'parents':
588 v = [safe_bytes(x) for x in v]
590 v = [safe_bytes(x) for x in v]
589 else:
591 else:
590 v = safe_bytes(v)
592 v = safe_bytes(v)
591 setattr(commit, k, v)
593 setattr(commit, k, v)
592
594
593 object_store.add_object(commit)
595 object_store.add_object(commit)
594
596
595 self.create_branch(wire, branch, safe_str(commit.id))
597 self.create_branch(wire, branch, safe_str(commit.id))
596
598
597 # dulwich set-ref
599 # dulwich set-ref
598 repo.refs[safe_bytes(f'refs/heads/{branch}')] = commit.id
600 repo.refs[safe_bytes(f'refs/heads/{branch}')] = commit.id
599
601
600 return commit.id
602 return commit.id
601
603
602 @reraise_safe_exceptions
604 @reraise_safe_exceptions
603 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
605 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
604 if url != 'default' and '://' not in url:
606 if url != 'default' and '://' not in url:
605 client = LocalGitClient(url)
607 client = LocalGitClient(url)
606 else:
608 else:
607 url_obj = url_parser(url)
609 url_obj = url_parser(url)
608 o = self._build_opener(url)
610 o = self._build_opener(url)
609 url, _ = url_obj.authinfo()
611 url, _ = url_obj.authinfo()
610 client = HttpGitClient(base_url=url, opener=o)
612 client = HttpGitClient(base_url=url, opener=o)
611 repo = self._factory.repo(wire)
613 repo = self._factory.repo(wire)
612
614
613 determine_wants = repo.object_store.determine_wants_all
615 determine_wants = repo.object_store.determine_wants_all
614 if refs:
616 if refs:
615 refs = [ascii_bytes(x) for x in refs]
617 refs = [ascii_bytes(x) for x in refs]
616
618
617 def determine_wants_requested(remote_refs):
619 def determine_wants_requested(remote_refs):
618 determined = []
620 determined = []
619 for ref_name, ref_hash in remote_refs.items():
621 for ref_name, ref_hash in remote_refs.items():
620 bytes_ref_name = safe_bytes(ref_name)
622 bytes_ref_name = safe_bytes(ref_name)
621
623
622 if bytes_ref_name in refs:
624 if bytes_ref_name in refs:
623 bytes_ref_hash = safe_bytes(ref_hash)
625 bytes_ref_hash = safe_bytes(ref_hash)
624 determined.append(bytes_ref_hash)
626 determined.append(bytes_ref_hash)
625 return determined
627 return determined
626
628
627 # swap with our custom requested wants
629 # swap with our custom requested wants
628 determine_wants = determine_wants_requested
630 determine_wants = determine_wants_requested
629
631
630 try:
632 try:
631 remote_refs = client.fetch(
633 remote_refs = client.fetch(
632 path=url, target=repo, determine_wants=determine_wants)
634 path=url, target=repo, determine_wants=determine_wants)
633
635
634 except NotGitRepository as e:
636 except NotGitRepository as e:
635 log.warning(
637 log.warning(
636 'Trying to fetch from "%s" failed, not a Git repository.', url)
638 'Trying to fetch from "%s" failed, not a Git repository.', url)
637 # Exception can contain unicode which we convert
639 # Exception can contain unicode which we convert
638 raise exceptions.AbortException(e)(repr(e))
640 raise exceptions.AbortException(e)(repr(e))
639
641
640 # mikhail: client.fetch() returns all the remote refs, but fetches only
642 # mikhail: client.fetch() returns all the remote refs, but fetches only
641 # refs filtered by `determine_wants` function. We need to filter result
643 # refs filtered by `determine_wants` function. We need to filter result
642 # as well
644 # as well
643 if refs:
645 if refs:
644 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
646 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
645
647
646 if apply_refs:
648 if apply_refs:
647 # TODO: johbo: Needs proper test coverage with a git repository
649 # TODO: johbo: Needs proper test coverage with a git repository
648 # that contains a tag object, so that we would end up with
650 # that contains a tag object, so that we would end up with
649 # a peeled ref at this point.
651 # a peeled ref at this point.
650 for k in remote_refs:
652 for k in remote_refs:
651 if k.endswith(PEELED_REF_MARKER):
653 if k.endswith(PEELED_REF_MARKER):
652 log.debug("Skipping peeled reference %s", k)
654 log.debug("Skipping peeled reference %s", k)
653 continue
655 continue
654 repo[k] = remote_refs[k]
656 repo[k] = remote_refs[k]
655
657
656 if refs and not update_after:
658 if refs and not update_after:
657 # mikhail: explicitly set the head to the last ref.
659 # mikhail: explicitly set the head to the last ref.
658 repo[HEAD_MARKER] = remote_refs[refs[-1]]
660 repo[HEAD_MARKER] = remote_refs[refs[-1]]
659
661
660 if update_after:
662 if update_after:
661 # we want to checkout HEAD
663 # we want to check out HEAD
662 repo[HEAD_MARKER] = remote_refs[HEAD_MARKER]
664 repo[HEAD_MARKER] = remote_refs[HEAD_MARKER]
663 index.build_index_from_tree(repo.path, repo.index_path(),
665 index.build_index_from_tree(repo.path, repo.index_path(),
664 repo.object_store, repo[HEAD_MARKER].tree)
666 repo.object_store, repo[HEAD_MARKER].tree)
665 return remote_refs
667 return remote_refs
666
668
667 @reraise_safe_exceptions
669 @reraise_safe_exceptions
668 def sync_fetch(self, wire, url, refs=None, all_refs=False):
670 def sync_fetch(self, wire, url, refs=None, all_refs=False):
669 repo = self._factory.repo(wire)
671 repo = self._factory.repo(wire)
670 if refs and not isinstance(refs, (list, tuple)):
672 if refs and not isinstance(refs, (list, tuple)):
671 refs = [refs]
673 refs = [refs]
672
674
673 config = self._wire_to_config(wire)
675 config = self._wire_to_config(wire)
674 # get all remote refs we'll use to fetch later
676 # get all remote refs we'll use to fetch later
675 cmd = ['ls-remote']
677 cmd = ['ls-remote']
676 if not all_refs:
678 if not all_refs:
677 cmd += ['--heads', '--tags']
679 cmd += ['--heads', '--tags']
678 cmd += [url]
680 cmd += [url]
679 output, __ = self.run_git_command(
681 output, __ = self.run_git_command(
680 wire, cmd, fail_on_stderr=False,
682 wire, cmd, fail_on_stderr=False,
681 _copts=self._remote_conf(config),
683 _copts=self._remote_conf(config),
682 extra_env={'GIT_TERMINAL_PROMPT': '0'})
684 extra_env={'GIT_TERMINAL_PROMPT': '0'})
683
685
684 remote_refs = collections.OrderedDict()
686 remote_refs = collections.OrderedDict()
685 fetch_refs = []
687 fetch_refs = []
686
688
687 for ref_line in output.splitlines():
689 for ref_line in output.splitlines():
688 sha, ref = ref_line.split(b'\t')
690 sha, ref = ref_line.split(b'\t')
689 sha = sha.strip()
691 sha = sha.strip()
690 if ref in remote_refs:
692 if ref in remote_refs:
691 # duplicate, skip
693 # duplicate, skip
692 continue
694 continue
693 if ref.endswith(PEELED_REF_MARKER):
695 if ref.endswith(PEELED_REF_MARKER):
694 log.debug("Skipping peeled reference %s", ref)
696 log.debug("Skipping peeled reference %s", ref)
695 continue
697 continue
696 # don't sync HEAD
698 # don't sync HEAD
697 if ref in [HEAD_MARKER]:
699 if ref in [HEAD_MARKER]:
698 continue
700 continue
699
701
700 remote_refs[ref] = sha
702 remote_refs[ref] = sha
701
703
702 if refs and sha in refs:
704 if refs and sha in refs:
703 # we filter fetch using our specified refs
705 # we filter fetch using our specified refs
704 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
706 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
705 elif not refs:
707 elif not refs:
706 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
708 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
707 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
709 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
708
710
709 if fetch_refs:
711 if fetch_refs:
710 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
712 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
711 fetch_refs_chunks = list(chunk)
713 fetch_refs_chunks = list(chunk)
712 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
714 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
713 self.run_git_command(
715 self.run_git_command(
714 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
716 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
715 fail_on_stderr=False,
717 fail_on_stderr=False,
716 _copts=self._remote_conf(config),
718 _copts=self._remote_conf(config),
717 extra_env={'GIT_TERMINAL_PROMPT': '0'})
719 extra_env={'GIT_TERMINAL_PROMPT': '0'})
718
720
719 return remote_refs
721 return remote_refs
720
722
721 @reraise_safe_exceptions
723 @reraise_safe_exceptions
722 def sync_push(self, wire, url, refs=None):
724 def sync_push(self, wire, url, refs=None):
723 if not self.check_url(url, wire):
725 if not self.check_url(url, wire):
724 return
726 return
725 config = self._wire_to_config(wire)
727 config = self._wire_to_config(wire)
726 self._factory.repo(wire)
728 self._factory.repo(wire)
727 self.run_git_command(
729 self.run_git_command(
728 wire, ['push', url, '--mirror'], fail_on_stderr=False,
730 wire, ['push', url, '--mirror'], fail_on_stderr=False,
729 _copts=self._remote_conf(config),
731 _copts=self._remote_conf(config),
730 extra_env={'GIT_TERMINAL_PROMPT': '0'})
732 extra_env={'GIT_TERMINAL_PROMPT': '0'})
731
733
732 @reraise_safe_exceptions
734 @reraise_safe_exceptions
733 def get_remote_refs(self, wire, url):
735 def get_remote_refs(self, wire, url):
734 repo = Repo(url)
736 repo = Repo(url)
735 return repo.get_refs()
737 return repo.get_refs()
736
738
737 @reraise_safe_exceptions
739 @reraise_safe_exceptions
738 def get_description(self, wire):
740 def get_description(self, wire):
739 repo = self._factory.repo(wire)
741 repo = self._factory.repo(wire)
740 return repo.get_description()
742 return repo.get_description()
741
743
742 @reraise_safe_exceptions
744 @reraise_safe_exceptions
743 def get_missing_revs(self, wire, rev1, rev2, path2):
745 def get_missing_revs(self, wire, rev1, rev2, path2):
744 repo = self._factory.repo(wire)
746 repo = self._factory.repo(wire)
745 LocalGitClient(thin_packs=False).fetch(path2, repo)
747 LocalGitClient(thin_packs=False).fetch(path2, repo)
746
748
747 wire_remote = wire.copy()
749 wire_remote = wire.copy()
748 wire_remote['path'] = path2
750 wire_remote['path'] = path2
749 repo_remote = self._factory.repo(wire_remote)
751 repo_remote = self._factory.repo(wire_remote)
750 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
752 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
751
753
752 revs = [
754 revs = [
753 x.commit.id
755 x.commit.id
754 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
756 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
755 return revs
757 return revs
756
758
757 @reraise_safe_exceptions
759 @reraise_safe_exceptions
758 def get_object(self, wire, sha, maybe_unreachable=False):
760 def get_object(self, wire, sha, maybe_unreachable=False):
759 cache_on, context_uid, repo_id = self._cache_on(wire)
761 cache_on, context_uid, repo_id = self._cache_on(wire)
760 region = self._region(wire)
762 region = self._region(wire)
761
763
762 @region.conditional_cache_on_arguments(condition=cache_on)
764 @region.conditional_cache_on_arguments(condition=cache_on)
763 def _get_object(_context_uid, _repo_id, _sha):
765 def _get_object(_context_uid, _repo_id, _sha):
764 repo_init = self._factory.repo_libgit2(wire)
766 repo_init = self._factory.repo_libgit2(wire)
765 with repo_init as repo:
767 with repo_init as repo:
766
768
767 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
769 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
768 try:
770 try:
769 commit = repo.revparse_single(sha)
771 commit = repo.revparse_single(sha)
770 except KeyError:
772 except KeyError:
771 # NOTE(marcink): KeyError doesn't give us any meaningful information
773 # NOTE(marcink): KeyError doesn't give us any meaningful information
772 # here, we instead give something more explicit
774 # here, we instead give something more explicit
773 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
775 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
774 raise exceptions.LookupException(e)(missing_commit_err)
776 raise exceptions.LookupException(e)(missing_commit_err)
775 except ValueError as e:
777 except ValueError as e:
776 raise exceptions.LookupException(e)(missing_commit_err)
778 raise exceptions.LookupException(e)(missing_commit_err)
777
779
778 is_tag = False
780 is_tag = False
779 if isinstance(commit, pygit2.Tag):
781 if isinstance(commit, pygit2.Tag):
780 commit = repo.get(commit.target)
782 commit = repo.get(commit.target)
781 is_tag = True
783 is_tag = True
782
784
783 check_dangling = True
785 check_dangling = True
784 if is_tag:
786 if is_tag:
785 check_dangling = False
787 check_dangling = False
786
788
787 if check_dangling and maybe_unreachable:
789 if check_dangling and maybe_unreachable:
788 check_dangling = False
790 check_dangling = False
789
791
790 # we used a reference and it parsed means we're not having a dangling commit
792 # we used a reference and it parsed means we're not having a dangling commit
791 if sha != commit.hex:
793 if sha != commit.hex:
792 check_dangling = False
794 check_dangling = False
793
795
794 if check_dangling:
796 if check_dangling:
795 # check for dangling commit
797 # check for dangling commit
796 for branch in repo.branches.with_commit(commit.hex):
798 for branch in repo.branches.with_commit(commit.hex):
797 if branch:
799 if branch:
798 break
800 break
799 else:
801 else:
800 # NOTE(marcink): Empty error doesn't give us any meaningful information
802 # NOTE(marcink): Empty error doesn't give us any meaningful information
801 # here, we instead give something more explicit
803 # here, we instead give something more explicit
802 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
804 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
803 raise exceptions.LookupException(e)(missing_commit_err)
805 raise exceptions.LookupException(e)(missing_commit_err)
804
806
805 commit_id = commit.hex
807 commit_id = commit.hex
806 type_id = commit.type
808 type_id = commit.type
807
809
808 return {
810 return {
809 'id': commit_id,
811 'id': commit_id,
810 'type': self._type_id_to_name(type_id),
812 'type': self._type_id_to_name(type_id),
811 'commit_id': commit_id,
813 'commit_id': commit_id,
812 'idx': 0
814 'idx': 0
813 }
815 }
814
816
815 return _get_object(context_uid, repo_id, sha)
817 return _get_object(context_uid, repo_id, sha)
816
818
817 @reraise_safe_exceptions
819 @reraise_safe_exceptions
818 def get_refs(self, wire):
820 def get_refs(self, wire):
819 cache_on, context_uid, repo_id = self._cache_on(wire)
821 cache_on, context_uid, repo_id = self._cache_on(wire)
820 region = self._region(wire)
822 region = self._region(wire)
821
823
822 @region.conditional_cache_on_arguments(condition=cache_on)
824 @region.conditional_cache_on_arguments(condition=cache_on)
823 def _get_refs(_context_uid, _repo_id):
825 def _get_refs(_context_uid, _repo_id):
824
826
825 repo_init = self._factory.repo_libgit2(wire)
827 repo_init = self._factory.repo_libgit2(wire)
826 with repo_init as repo:
828 with repo_init as repo:
827 regex = re.compile('^refs/(heads|tags)/')
829 regex = re.compile('^refs/(heads|tags)/')
828 return {x.name: x.target.hex for x in
830 return {x.name: x.target.hex for x in
829 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
831 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
830
832
831 return _get_refs(context_uid, repo_id)
833 return _get_refs(context_uid, repo_id)
832
834
833 @reraise_safe_exceptions
835 @reraise_safe_exceptions
834 def get_branch_pointers(self, wire):
836 def get_branch_pointers(self, wire):
835 cache_on, context_uid, repo_id = self._cache_on(wire)
837 cache_on, context_uid, repo_id = self._cache_on(wire)
836 region = self._region(wire)
838 region = self._region(wire)
837
839
838 @region.conditional_cache_on_arguments(condition=cache_on)
840 @region.conditional_cache_on_arguments(condition=cache_on)
839 def _get_branch_pointers(_context_uid, _repo_id):
841 def _get_branch_pointers(_context_uid, _repo_id):
840
842
841 repo_init = self._factory.repo_libgit2(wire)
843 repo_init = self._factory.repo_libgit2(wire)
842 regex = re.compile('^refs/heads')
844 regex = re.compile('^refs/heads')
843 with repo_init as repo:
845 with repo_init as repo:
844 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
846 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
845 return {x.target.hex: x.shorthand for x in branches}
847 return {x.target.hex: x.shorthand for x in branches}
846
848
847 return _get_branch_pointers(context_uid, repo_id)
849 return _get_branch_pointers(context_uid, repo_id)
848
850
849 @reraise_safe_exceptions
851 @reraise_safe_exceptions
850 def head(self, wire, show_exc=True):
852 def head(self, wire, show_exc=True):
851 cache_on, context_uid, repo_id = self._cache_on(wire)
853 cache_on, context_uid, repo_id = self._cache_on(wire)
852 region = self._region(wire)
854 region = self._region(wire)
853
855
854 @region.conditional_cache_on_arguments(condition=cache_on)
856 @region.conditional_cache_on_arguments(condition=cache_on)
855 def _head(_context_uid, _repo_id, _show_exc):
857 def _head(_context_uid, _repo_id, _show_exc):
856 repo_init = self._factory.repo_libgit2(wire)
858 repo_init = self._factory.repo_libgit2(wire)
857 with repo_init as repo:
859 with repo_init as repo:
858 try:
860 try:
859 return repo.head.peel().hex
861 return repo.head.peel().hex
860 except Exception:
862 except Exception:
861 if show_exc:
863 if show_exc:
862 raise
864 raise
863 return _head(context_uid, repo_id, show_exc)
865 return _head(context_uid, repo_id, show_exc)
864
866
865 @reraise_safe_exceptions
867 @reraise_safe_exceptions
866 def init(self, wire):
868 def init(self, wire):
867 repo_path = safe_str(wire['path'])
869 repo_path = safe_str(wire['path'])
868 self.repo = Repo.init(repo_path)
870 self.repo = Repo.init(repo_path)
869
871
870 @reraise_safe_exceptions
872 @reraise_safe_exceptions
871 def init_bare(self, wire):
873 def init_bare(self, wire):
872 repo_path = safe_str(wire['path'])
874 repo_path = safe_str(wire['path'])
873 self.repo = Repo.init_bare(repo_path)
875 self.repo = Repo.init_bare(repo_path)
874
876
875 @reraise_safe_exceptions
877 @reraise_safe_exceptions
876 def revision(self, wire, rev):
878 def revision(self, wire, rev):
877
879
878 cache_on, context_uid, repo_id = self._cache_on(wire)
880 cache_on, context_uid, repo_id = self._cache_on(wire)
879 region = self._region(wire)
881 region = self._region(wire)
880
882
881 @region.conditional_cache_on_arguments(condition=cache_on)
883 @region.conditional_cache_on_arguments(condition=cache_on)
882 def _revision(_context_uid, _repo_id, _rev):
884 def _revision(_context_uid, _repo_id, _rev):
883 repo_init = self._factory.repo_libgit2(wire)
885 repo_init = self._factory.repo_libgit2(wire)
884 with repo_init as repo:
886 with repo_init as repo:
885 commit = repo[rev]
887 commit = repo[rev]
886 obj_data = {
888 obj_data = {
887 'id': commit.id.hex,
889 'id': commit.id.hex,
888 }
890 }
889 # tree objects itself don't have tree_id attribute
891 # tree objects itself don't have tree_id attribute
890 if hasattr(commit, 'tree_id'):
892 if hasattr(commit, 'tree_id'):
891 obj_data['tree'] = commit.tree_id.hex
893 obj_data['tree'] = commit.tree_id.hex
892
894
893 return obj_data
895 return obj_data
894 return _revision(context_uid, repo_id, rev)
896 return _revision(context_uid, repo_id, rev)
895
897
896 @reraise_safe_exceptions
898 @reraise_safe_exceptions
897 def date(self, wire, commit_id):
899 def date(self, wire, commit_id):
898 cache_on, context_uid, repo_id = self._cache_on(wire)
900 cache_on, context_uid, repo_id = self._cache_on(wire)
899 region = self._region(wire)
901 region = self._region(wire)
900
902
901 @region.conditional_cache_on_arguments(condition=cache_on)
903 @region.conditional_cache_on_arguments(condition=cache_on)
902 def _date(_repo_id, _commit_id):
904 def _date(_repo_id, _commit_id):
903 repo_init = self._factory.repo_libgit2(wire)
905 repo_init = self._factory.repo_libgit2(wire)
904 with repo_init as repo:
906 with repo_init as repo:
905 commit = repo[commit_id]
907 commit = repo[commit_id]
906
908
907 if hasattr(commit, 'commit_time'):
909 if hasattr(commit, 'commit_time'):
908 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
910 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
909 else:
911 else:
910 commit = commit.get_object()
912 commit = commit.get_object()
911 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
913 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
912
914
913 # TODO(marcink): check dulwich difference of offset vs timezone
915 # TODO(marcink): check dulwich difference of offset vs timezone
914 return [commit_time, commit_time_offset]
916 return [commit_time, commit_time_offset]
915 return _date(repo_id, commit_id)
917 return _date(repo_id, commit_id)
916
918
917 @reraise_safe_exceptions
919 @reraise_safe_exceptions
918 def author(self, wire, commit_id):
920 def author(self, wire, commit_id):
919 cache_on, context_uid, repo_id = self._cache_on(wire)
921 cache_on, context_uid, repo_id = self._cache_on(wire)
920 region = self._region(wire)
922 region = self._region(wire)
921
923
922 @region.conditional_cache_on_arguments(condition=cache_on)
924 @region.conditional_cache_on_arguments(condition=cache_on)
923 def _author(_repo_id, _commit_id):
925 def _author(_repo_id, _commit_id):
924 repo_init = self._factory.repo_libgit2(wire)
926 repo_init = self._factory.repo_libgit2(wire)
925 with repo_init as repo:
927 with repo_init as repo:
926 commit = repo[commit_id]
928 commit = repo[commit_id]
927
929
928 if hasattr(commit, 'author'):
930 if hasattr(commit, 'author'):
929 author = commit.author
931 author = commit.author
930 else:
932 else:
931 author = commit.get_object().author
933 author = commit.get_object().author
932
934
933 if author.email:
935 if author.email:
934 return "{} <{}>".format(author.name, author.email)
936 return "{} <{}>".format(author.name, author.email)
935
937
936 try:
938 try:
937 return "{}".format(author.name)
939 return "{}".format(author.name)
938 except Exception:
940 except Exception:
939 return "{}".format(safe_str(author.raw_name))
941 return "{}".format(safe_str(author.raw_name))
940
942
941 return _author(repo_id, commit_id)
943 return _author(repo_id, commit_id)
942
944
943 @reraise_safe_exceptions
945 @reraise_safe_exceptions
944 def message(self, wire, commit_id):
946 def message(self, wire, commit_id):
945 cache_on, context_uid, repo_id = self._cache_on(wire)
947 cache_on, context_uid, repo_id = self._cache_on(wire)
946 region = self._region(wire)
948 region = self._region(wire)
949
947 @region.conditional_cache_on_arguments(condition=cache_on)
950 @region.conditional_cache_on_arguments(condition=cache_on)
948 def _message(_repo_id, _commit_id):
951 def _message(_repo_id, _commit_id):
949 repo_init = self._factory.repo_libgit2(wire)
952 repo_init = self._factory.repo_libgit2(wire)
950 with repo_init as repo:
953 with repo_init as repo:
951 commit = repo[commit_id]
954 commit = repo[commit_id]
952 return commit.message
955 return commit.message
953 return _message(repo_id, commit_id)
956 return _message(repo_id, commit_id)
954
957
955 @reraise_safe_exceptions
958 @reraise_safe_exceptions
956 def parents(self, wire, commit_id):
959 def parents(self, wire, commit_id):
957 cache_on, context_uid, repo_id = self._cache_on(wire)
960 cache_on, context_uid, repo_id = self._cache_on(wire)
958 region = self._region(wire)
961 region = self._region(wire)
959
962
960 @region.conditional_cache_on_arguments(condition=cache_on)
963 @region.conditional_cache_on_arguments(condition=cache_on)
961 def _parents(_repo_id, _commit_id):
964 def _parents(_repo_id, _commit_id):
962 repo_init = self._factory.repo_libgit2(wire)
965 repo_init = self._factory.repo_libgit2(wire)
963 with repo_init as repo:
966 with repo_init as repo:
964 commit = repo[commit_id]
967 commit = repo[commit_id]
965 if hasattr(commit, 'parent_ids'):
968 if hasattr(commit, 'parent_ids'):
966 parent_ids = commit.parent_ids
969 parent_ids = commit.parent_ids
967 else:
970 else:
968 parent_ids = commit.get_object().parent_ids
971 parent_ids = commit.get_object().parent_ids
969
972
970 return [x.hex for x in parent_ids]
973 return [x.hex for x in parent_ids]
971 return _parents(repo_id, commit_id)
974 return _parents(repo_id, commit_id)
972
975
973 @reraise_safe_exceptions
976 @reraise_safe_exceptions
974 def children(self, wire, commit_id):
977 def children(self, wire, commit_id):
975 cache_on, context_uid, repo_id = self._cache_on(wire)
978 cache_on, context_uid, repo_id = self._cache_on(wire)
976 region = self._region(wire)
979 region = self._region(wire)
977
980
978 head = self.head(wire)
981 head = self.head(wire)
979
982
980 @region.conditional_cache_on_arguments(condition=cache_on)
983 @region.conditional_cache_on_arguments(condition=cache_on)
981 def _children(_repo_id, _commit_id):
984 def _children(_repo_id, _commit_id):
982
985
983 output, __ = self.run_git_command(
986 output, __ = self.run_git_command(
984 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
987 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
985
988
986 child_ids = []
989 child_ids = []
987 pat = re.compile(r'^{}'.format(commit_id))
990 pat = re.compile(r'^{}'.format(commit_id))
988 for line in output.splitlines():
991 for line in output.splitlines():
989 line = safe_str(line)
992 line = safe_str(line)
990 if pat.match(line):
993 if pat.match(line):
991 found_ids = line.split(' ')[1:]
994 found_ids = line.split(' ')[1:]
992 child_ids.extend(found_ids)
995 child_ids.extend(found_ids)
993 break
996 break
994
997
995 return child_ids
998 return child_ids
996 return _children(repo_id, commit_id)
999 return _children(repo_id, commit_id)
997
1000
998 @reraise_safe_exceptions
1001 @reraise_safe_exceptions
999 def set_refs(self, wire, key, value):
1002 def set_refs(self, wire, key, value):
1000 repo_init = self._factory.repo_libgit2(wire)
1003 repo_init = self._factory.repo_libgit2(wire)
1001 with repo_init as repo:
1004 with repo_init as repo:
1002 repo.references.create(key, value, force=True)
1005 repo.references.create(key, value, force=True)
1003
1006
1004 @reraise_safe_exceptions
1007 @reraise_safe_exceptions
1005 def create_branch(self, wire, branch_name, commit_id, force=False):
1008 def create_branch(self, wire, branch_name, commit_id, force=False):
1006 repo_init = self._factory.repo_libgit2(wire)
1009 repo_init = self._factory.repo_libgit2(wire)
1007 with repo_init as repo:
1010 with repo_init as repo:
1008 commit = repo[commit_id]
1011 commit = repo[commit_id]
1009
1012
1010 if force:
1013 if force:
1011 repo.branches.local.create(branch_name, commit, force=force)
1014 repo.branches.local.create(branch_name, commit, force=force)
1012 elif not repo.branches.get(branch_name):
1015 elif not repo.branches.get(branch_name):
1013 # create only if that branch isn't existing
1016 # create only if that branch isn't existing
1014 repo.branches.local.create(branch_name, commit, force=force)
1017 repo.branches.local.create(branch_name, commit, force=force)
1015
1018
1016 @reraise_safe_exceptions
1019 @reraise_safe_exceptions
1017 def remove_ref(self, wire, key):
1020 def remove_ref(self, wire, key):
1018 repo_init = self._factory.repo_libgit2(wire)
1021 repo_init = self._factory.repo_libgit2(wire)
1019 with repo_init as repo:
1022 with repo_init as repo:
1020 repo.references.delete(key)
1023 repo.references.delete(key)
1021
1024
1022 @reraise_safe_exceptions
1025 @reraise_safe_exceptions
1023 def tag_remove(self, wire, tag_name):
1026 def tag_remove(self, wire, tag_name):
1024 repo_init = self._factory.repo_libgit2(wire)
1027 repo_init = self._factory.repo_libgit2(wire)
1025 with repo_init as repo:
1028 with repo_init as repo:
1026 key = 'refs/tags/{}'.format(tag_name)
1029 key = 'refs/tags/{}'.format(tag_name)
1027 repo.references.delete(key)
1030 repo.references.delete(key)
1028
1031
1029 @reraise_safe_exceptions
1032 @reraise_safe_exceptions
1030 def tree_changes(self, wire, source_id, target_id):
1033 def tree_changes(self, wire, source_id, target_id):
1031 # TODO(marcink): remove this seems it's only used by tests
1034 # TODO(marcink): remove this seems it's only used by tests
1032 repo = self._factory.repo(wire)
1035 repo = self._factory.repo(wire)
1033 source = repo[source_id].tree if source_id else None
1036 source = repo[source_id].tree if source_id else None
1034 target = repo[target_id].tree
1037 target = repo[target_id].tree
1035 result = repo.object_store.tree_changes(source, target)
1038 result = repo.object_store.tree_changes(source, target)
1036 return list(result)
1039 return list(result)
1037
1040
1038 @reraise_safe_exceptions
1041 @reraise_safe_exceptions
1039 def tree_and_type_for_path(self, wire, commit_id, path):
1042 def tree_and_type_for_path(self, wire, commit_id, path):
1040
1043
1041 cache_on, context_uid, repo_id = self._cache_on(wire)
1044 cache_on, context_uid, repo_id = self._cache_on(wire)
1042 region = self._region(wire)
1045 region = self._region(wire)
1043
1046
1044 @region.conditional_cache_on_arguments(condition=cache_on)
1047 @region.conditional_cache_on_arguments(condition=cache_on)
1045 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1048 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1046 repo_init = self._factory.repo_libgit2(wire)
1049 repo_init = self._factory.repo_libgit2(wire)
1047
1050
1048 with repo_init as repo:
1051 with repo_init as repo:
1049 commit = repo[commit_id]
1052 commit = repo[commit_id]
1050 try:
1053 try:
1051 tree = commit.tree[path]
1054 tree = commit.tree[path]
1052 except KeyError:
1055 except KeyError:
1053 return None, None, None
1056 return None, None, None
1054
1057
1055 return tree.id.hex, tree.type_str, tree.filemode
1058 return tree.id.hex, tree.type_str, tree.filemode
1056 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1059 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1057
1060
1058 @reraise_safe_exceptions
1061 @reraise_safe_exceptions
1059 def tree_items(self, wire, tree_id):
1062 def tree_items(self, wire, tree_id):
1060 cache_on, context_uid, repo_id = self._cache_on(wire)
1063 cache_on, context_uid, repo_id = self._cache_on(wire)
1061 region = self._region(wire)
1064 region = self._region(wire)
1062
1065
1063 @region.conditional_cache_on_arguments(condition=cache_on)
1066 @region.conditional_cache_on_arguments(condition=cache_on)
1064 def _tree_items(_repo_id, _tree_id):
1067 def _tree_items(_repo_id, _tree_id):
1065
1068
1066 repo_init = self._factory.repo_libgit2(wire)
1069 repo_init = self._factory.repo_libgit2(wire)
1067 with repo_init as repo:
1070 with repo_init as repo:
1068 try:
1071 try:
1069 tree = repo[tree_id]
1072 tree = repo[tree_id]
1070 except KeyError:
1073 except KeyError:
1071 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1074 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1072
1075
1073 result = []
1076 result = []
1074 for item in tree:
1077 for item in tree:
1075 item_sha = item.hex
1078 item_sha = item.hex
1076 item_mode = item.filemode
1079 item_mode = item.filemode
1077 item_type = item.type_str
1080 item_type = item.type_str
1078
1081
1079 if item_type == 'commit':
1082 if item_type == 'commit':
1080 # NOTE(marcink): submodules we translate to 'link' for backward compat
1083 # NOTE(marcink): submodules we translate to 'link' for backward compat
1081 item_type = 'link'
1084 item_type = 'link'
1082
1085
1083 result.append((item.name, item_mode, item_sha, item_type))
1086 result.append((item.name, item_mode, item_sha, item_type))
1084 return result
1087 return result
1085 return _tree_items(repo_id, tree_id)
1088 return _tree_items(repo_id, tree_id)
1086
1089
1087 @reraise_safe_exceptions
1090 @reraise_safe_exceptions
1088 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1091 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1089 """
1092 """
1090 Old version that uses subprocess to call diff
1093 Old version that uses subprocess to call diff
1091 """
1094 """
1092
1095
1093 flags = [
1096 flags = [
1094 '-U%s' % context, '--patch',
1097 '-U%s' % context, '--patch',
1095 '--binary',
1098 '--binary',
1096 '--find-renames',
1099 '--find-renames',
1097 '--no-indent-heuristic',
1100 '--no-indent-heuristic',
1098 # '--indent-heuristic',
1101 # '--indent-heuristic',
1099 #'--full-index',
1102 #'--full-index',
1100 #'--abbrev=40'
1103 #'--abbrev=40'
1101 ]
1104 ]
1102
1105
1103 if opt_ignorews:
1106 if opt_ignorews:
1104 flags.append('--ignore-all-space')
1107 flags.append('--ignore-all-space')
1105
1108
1106 if commit_id_1 == self.EMPTY_COMMIT:
1109 if commit_id_1 == self.EMPTY_COMMIT:
1107 cmd = ['show'] + flags + [commit_id_2]
1110 cmd = ['show'] + flags + [commit_id_2]
1108 else:
1111 else:
1109 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1112 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1110
1113
1111 if file_filter:
1114 if file_filter:
1112 cmd.extend(['--', file_filter])
1115 cmd.extend(['--', file_filter])
1113
1116
1114 diff, __ = self.run_git_command(wire, cmd)
1117 diff, __ = self.run_git_command(wire, cmd)
1115 # If we used 'show' command, strip first few lines (until actual diff
1118 # If we used 'show' command, strip first few lines (until actual diff
1116 # starts)
1119 # starts)
1117 if commit_id_1 == self.EMPTY_COMMIT:
1120 if commit_id_1 == self.EMPTY_COMMIT:
1118 lines = diff.splitlines()
1121 lines = diff.splitlines()
1119 x = 0
1122 x = 0
1120 for line in lines:
1123 for line in lines:
1121 if line.startswith(b'diff'):
1124 if line.startswith(b'diff'):
1122 break
1125 break
1123 x += 1
1126 x += 1
1124 # Append new line just like 'diff' command do
1127 # Append new line just like 'diff' command do
1125 diff = '\n'.join(lines[x:]) + '\n'
1128 diff = '\n'.join(lines[x:]) + '\n'
1126 return diff
1129 return diff
1127
1130
1128 @reraise_safe_exceptions
1131 @reraise_safe_exceptions
1129 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1132 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1130 repo_init = self._factory.repo_libgit2(wire)
1133 repo_init = self._factory.repo_libgit2(wire)
1131
1134
1132 with repo_init as repo:
1135 with repo_init as repo:
1133 swap = True
1136 swap = True
1134 flags = 0
1137 flags = 0
1135 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1138 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1136
1139
1137 if opt_ignorews:
1140 if opt_ignorews:
1138 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1141 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1139
1142
1140 if commit_id_1 == self.EMPTY_COMMIT:
1143 if commit_id_1 == self.EMPTY_COMMIT:
1141 comm1 = repo[commit_id_2]
1144 comm1 = repo[commit_id_2]
1142 diff_obj = comm1.tree.diff_to_tree(
1145 diff_obj = comm1.tree.diff_to_tree(
1143 flags=flags, context_lines=context, swap=swap)
1146 flags=flags, context_lines=context, swap=swap)
1144
1147
1145 else:
1148 else:
1146 comm1 = repo[commit_id_2]
1149 comm1 = repo[commit_id_2]
1147 comm2 = repo[commit_id_1]
1150 comm2 = repo[commit_id_1]
1148 diff_obj = comm1.tree.diff_to_tree(
1151 diff_obj = comm1.tree.diff_to_tree(
1149 comm2.tree, flags=flags, context_lines=context, swap=swap)
1152 comm2.tree, flags=flags, context_lines=context, swap=swap)
1150 similar_flags = 0
1153 similar_flags = 0
1151 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1154 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1152 diff_obj.find_similar(flags=similar_flags)
1155 diff_obj.find_similar(flags=similar_flags)
1153
1156
1154 if file_filter:
1157 if file_filter:
1155 for p in diff_obj:
1158 for p in diff_obj:
1156 if p.delta.old_file.path == file_filter:
1159 if p.delta.old_file.path == file_filter:
1157 return BinaryEnvelope(p.data) or BinaryEnvelope(b'')
1160 return BinaryEnvelope(p.data) or BinaryEnvelope(b'')
1158 # fo matching path == no diff
1161 # fo matching path == no diff
1159 return BinaryEnvelope(b'')
1162 return BinaryEnvelope(b'')
1160 return BinaryEnvelope(diff_obj.patch) or BinaryEnvelope(b'')
1163 return BinaryEnvelope(diff_obj.patch) or BinaryEnvelope(b'')
1161
1164
1162 @reraise_safe_exceptions
1165 @reraise_safe_exceptions
1163 def node_history(self, wire, commit_id, path, limit):
1166 def node_history(self, wire, commit_id, path, limit):
1164 cache_on, context_uid, repo_id = self._cache_on(wire)
1167 cache_on, context_uid, repo_id = self._cache_on(wire)
1165 region = self._region(wire)
1168 region = self._region(wire)
1166
1169
1167 @region.conditional_cache_on_arguments(condition=cache_on)
1170 @region.conditional_cache_on_arguments(condition=cache_on)
1168 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1171 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1169 # optimize for n==1, rev-list is much faster for that use-case
1172 # optimize for n==1, rev-list is much faster for that use-case
1170 if limit == 1:
1173 if limit == 1:
1171 cmd = ['rev-list', '-1', commit_id, '--', path]
1174 cmd = ['rev-list', '-1', commit_id, '--', path]
1172 else:
1175 else:
1173 cmd = ['log']
1176 cmd = ['log']
1174 if limit:
1177 if limit:
1175 cmd.extend(['-n', str(safe_int(limit, 0))])
1178 cmd.extend(['-n', str(safe_int(limit, 0))])
1176 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1179 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1177
1180
1178 output, __ = self.run_git_command(wire, cmd)
1181 output, __ = self.run_git_command(wire, cmd)
1179 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1182 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1180
1183
1181 return [x for x in commit_ids]
1184 return [x for x in commit_ids]
1182 return _node_history(context_uid, repo_id, commit_id, path, limit)
1185 return _node_history(context_uid, repo_id, commit_id, path, limit)
1183
1186
1184 @reraise_safe_exceptions
1187 @reraise_safe_exceptions
1185 def node_annotate_legacy(self, wire, commit_id, path):
1188 def node_annotate_legacy(self, wire, commit_id, path):
1186 #note: replaced by pygit2 impelementation
1189 # note: replaced by pygit2 implementation
1187 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1190 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1188 # -l ==> outputs long shas (and we need all 40 characters)
1191 # -l ==> outputs long shas (and we need all 40 characters)
1189 # --root ==> doesn't put '^' character for boundaries
1192 # --root ==> doesn't put '^' character for boundaries
1190 # -r commit_id ==> blames for the given commit
1193 # -r commit_id ==> blames for the given commit
1191 output, __ = self.run_git_command(wire, cmd)
1194 output, __ = self.run_git_command(wire, cmd)
1192
1195
1193 result = []
1196 result = []
1194 for i, blame_line in enumerate(output.splitlines()[:-1]):
1197 for i, blame_line in enumerate(output.splitlines()[:-1]):
1195 line_no = i + 1
1198 line_no = i + 1
1196 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1199 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1197 result.append((line_no, blame_commit_id, line))
1200 result.append((line_no, blame_commit_id, line))
1198
1201
1199 return result
1202 return result
1200
1203
1201 @reraise_safe_exceptions
1204 @reraise_safe_exceptions
1202 def node_annotate(self, wire, commit_id, path):
1205 def node_annotate(self, wire, commit_id, path):
1203
1206
1204 result_libgit = []
1207 result_libgit = []
1205 repo_init = self._factory.repo_libgit2(wire)
1208 repo_init = self._factory.repo_libgit2(wire)
1206 with repo_init as repo:
1209 with repo_init as repo:
1207 commit = repo[commit_id]
1210 commit = repo[commit_id]
1208 blame_obj = repo.blame(path, newest_commit=commit_id)
1211 blame_obj = repo.blame(path, newest_commit=commit_id)
1209 for i, line in enumerate(commit.tree[path].data.splitlines()):
1212 for i, line in enumerate(commit.tree[path].data.splitlines()):
1210 line_no = i + 1
1213 line_no = i + 1
1211 hunk = blame_obj.for_line(line_no)
1214 hunk = blame_obj.for_line(line_no)
1212 blame_commit_id = hunk.final_commit_id.hex
1215 blame_commit_id = hunk.final_commit_id.hex
1213
1216
1214 result_libgit.append((line_no, blame_commit_id, line))
1217 result_libgit.append((line_no, blame_commit_id, line))
1215
1218
1216 return result_libgit
1219 return result_libgit
1217
1220
1218 @reraise_safe_exceptions
1221 @reraise_safe_exceptions
1219 def update_server_info(self, wire):
1222 def update_server_info(self, wire):
1220 repo = self._factory.repo(wire)
1223 repo = self._factory.repo(wire)
1221 update_server_info(repo)
1224 update_server_info(repo)
1222
1225
1223 @reraise_safe_exceptions
1226 @reraise_safe_exceptions
1224 def get_all_commit_ids(self, wire):
1227 def get_all_commit_ids(self, wire):
1225
1228
1226 cache_on, context_uid, repo_id = self._cache_on(wire)
1229 cache_on, context_uid, repo_id = self._cache_on(wire)
1227 region = self._region(wire)
1230 region = self._region(wire)
1228
1231
1229 @region.conditional_cache_on_arguments(condition=cache_on)
1232 @region.conditional_cache_on_arguments(condition=cache_on)
1230 def _get_all_commit_ids(_context_uid, _repo_id):
1233 def _get_all_commit_ids(_context_uid, _repo_id):
1231
1234
1232 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1235 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1233 try:
1236 try:
1234 output, __ = self.run_git_command(wire, cmd)
1237 output, __ = self.run_git_command(wire, cmd)
1235 return output.splitlines()
1238 return output.splitlines()
1236 except Exception:
1239 except Exception:
1237 # Can be raised for empty repositories
1240 # Can be raised for empty repositories
1238 return []
1241 return []
1239
1242
1240 @region.conditional_cache_on_arguments(condition=cache_on)
1243 @region.conditional_cache_on_arguments(condition=cache_on)
1241 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1244 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1242 repo_init = self._factory.repo_libgit2(wire)
1245 repo_init = self._factory.repo_libgit2(wire)
1243 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1246 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1244 results = []
1247 results = []
1245 with repo_init as repo:
1248 with repo_init as repo:
1246 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1249 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1247 results.append(commit.id.hex)
1250 results.append(commit.id.hex)
1248
1251
1249 return _get_all_commit_ids(context_uid, repo_id)
1252 return _get_all_commit_ids(context_uid, repo_id)
1250
1253
1251 @reraise_safe_exceptions
1254 @reraise_safe_exceptions
1252 def run_git_command(self, wire, cmd, **opts):
1255 def run_git_command(self, wire, cmd, **opts):
1253 path = wire.get('path', None)
1256 path = wire.get('path', None)
1254
1257
1255 if path and os.path.isdir(path):
1258 if path and os.path.isdir(path):
1256 opts['cwd'] = path
1259 opts['cwd'] = path
1257
1260
1258 if '_bare' in opts:
1261 if '_bare' in opts:
1259 _copts = []
1262 _copts = []
1260 del opts['_bare']
1263 del opts['_bare']
1261 else:
1264 else:
1262 _copts = ['-c', 'core.quotepath=false', ]
1265 _copts = ['-c', 'core.quotepath=false', ]
1263 safe_call = False
1266 safe_call = False
1264 if '_safe' in opts:
1267 if '_safe' in opts:
1265 # no exc on failure
1268 # no exc on failure
1266 del opts['_safe']
1269 del opts['_safe']
1267 safe_call = True
1270 safe_call = True
1268
1271
1269 if '_copts' in opts:
1272 if '_copts' in opts:
1270 _copts.extend(opts['_copts'] or [])
1273 _copts.extend(opts['_copts'] or [])
1271 del opts['_copts']
1274 del opts['_copts']
1272
1275
1273 gitenv = os.environ.copy()
1276 gitenv = os.environ.copy()
1274 gitenv.update(opts.pop('extra_env', {}))
1277 gitenv.update(opts.pop('extra_env', {}))
1275 # need to clean fix GIT_DIR !
1278 # need to clean fix GIT_DIR !
1276 if 'GIT_DIR' in gitenv:
1279 if 'GIT_DIR' in gitenv:
1277 del gitenv['GIT_DIR']
1280 del gitenv['GIT_DIR']
1278 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1281 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1279 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1282 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1280
1283
1281 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1284 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1282 _opts = {'env': gitenv, 'shell': False}
1285 _opts = {'env': gitenv, 'shell': False}
1283
1286
1284 proc = None
1287 proc = None
1285 try:
1288 try:
1286 _opts.update(opts)
1289 _opts.update(opts)
1287 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1290 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1288
1291
1289 return b''.join(proc), b''.join(proc.stderr)
1292 return b''.join(proc), b''.join(proc.stderr)
1290 except OSError as err:
1293 except OSError as err:
1291 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1294 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1292 tb_err = ("Couldn't run git command (%s).\n"
1295 tb_err = ("Couldn't run git command (%s).\n"
1293 "Original error was:%s\n"
1296 "Original error was:%s\n"
1294 "Call options:%s\n"
1297 "Call options:%s\n"
1295 % (cmd, err, _opts))
1298 % (cmd, err, _opts))
1296 log.exception(tb_err)
1299 log.exception(tb_err)
1297 if safe_call:
1300 if safe_call:
1298 return '', err
1301 return '', err
1299 else:
1302 else:
1300 raise exceptions.VcsException()(tb_err)
1303 raise exceptions.VcsException()(tb_err)
1301 finally:
1304 finally:
1302 if proc:
1305 if proc:
1303 proc.close()
1306 proc.close()
1304
1307
1305 @reraise_safe_exceptions
1308 @reraise_safe_exceptions
1306 def install_hooks(self, wire, force=False):
1309 def install_hooks(self, wire, force=False):
1307 from vcsserver.hook_utils import install_git_hooks
1310 from vcsserver.hook_utils import install_git_hooks
1308 bare = self.bare(wire)
1311 bare = self.bare(wire)
1309 path = wire['path']
1312 path = wire['path']
1310 binary_dir = settings.BINARY_DIR
1313 binary_dir = settings.BINARY_DIR
1311 executable = None
1314 executable = None
1312 if binary_dir:
1315 if binary_dir:
1313 executable = os.path.join(binary_dir, 'python3')
1316 executable = os.path.join(binary_dir, 'python3')
1314 return install_git_hooks(path, bare, force_create=force)
1317 return install_git_hooks(path, bare, force_create=force)
1315
1318
1316 @reraise_safe_exceptions
1319 @reraise_safe_exceptions
1317 def get_hooks_info(self, wire):
1320 def get_hooks_info(self, wire):
1318 from vcsserver.hook_utils import (
1321 from vcsserver.hook_utils import (
1319 get_git_pre_hook_version, get_git_post_hook_version)
1322 get_git_pre_hook_version, get_git_post_hook_version)
1320 bare = self.bare(wire)
1323 bare = self.bare(wire)
1321 path = wire['path']
1324 path = wire['path']
1322 return {
1325 return {
1323 'pre_version': get_git_pre_hook_version(path, bare),
1326 'pre_version': get_git_pre_hook_version(path, bare),
1324 'post_version': get_git_post_hook_version(path, bare),
1327 'post_version': get_git_post_hook_version(path, bare),
1325 }
1328 }
1326
1329
1327 @reraise_safe_exceptions
1330 @reraise_safe_exceptions
1328 def set_head_ref(self, wire, head_name):
1331 def set_head_ref(self, wire, head_name):
1329 log.debug('Setting refs/head to `%s`', head_name)
1332 log.debug('Setting refs/head to `%s`', head_name)
1330 cmd = ['symbolic-ref', '"HEAD"', '"refs/heads/%s"' % head_name]
1333 cmd = ['symbolic-ref', '"HEAD"', '"refs/heads/%s"' % head_name]
1331 output, __ = self.run_git_command(wire, cmd)
1334 output, __ = self.run_git_command(wire, cmd)
1332 return [head_name] + output.splitlines()
1335 return [head_name] + output.splitlines()
1333
1336
1334 @reraise_safe_exceptions
1337 @reraise_safe_exceptions
1335 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1338 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1336 archive_dir_name, commit_id):
1339 archive_dir_name, commit_id):
1337
1340
1338 def file_walker(_commit_id, path):
1341 def file_walker(_commit_id, path):
1339 repo_init = self._factory.repo_libgit2(wire)
1342 repo_init = self._factory.repo_libgit2(wire)
1340
1343
1341 with repo_init as repo:
1344 with repo_init as repo:
1342 commit = repo[commit_id]
1345 commit = repo[commit_id]
1343
1346
1344 if path in ['', '/']:
1347 if path in ['', '/']:
1345 tree = commit.tree
1348 tree = commit.tree
1346 else:
1349 else:
1347 tree = commit.tree[path.rstrip('/')]
1350 tree = commit.tree[path.rstrip('/')]
1348 tree_id = tree.id.hex
1351 tree_id = tree.id.hex
1349 try:
1352 try:
1350 tree = repo[tree_id]
1353 tree = repo[tree_id]
1351 except KeyError:
1354 except KeyError:
1352 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1355 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1353
1356
1354 index = LibGit2Index.Index()
1357 index = LibGit2Index.Index()
1355 index.read_tree(tree)
1358 index.read_tree(tree)
1356 file_iter = index
1359 file_iter = index
1357
1360
1358 for fn in file_iter:
1361 for fn in file_iter:
1359 file_path = fn.path
1362 file_path = fn.path
1360 mode = fn.mode
1363 mode = fn.mode
1361 is_link = stat.S_ISLNK(mode)
1364 is_link = stat.S_ISLNK(mode)
1362 if mode == pygit2.GIT_FILEMODE_COMMIT:
1365 if mode == pygit2.GIT_FILEMODE_COMMIT:
1363 log.debug('Skipping path %s as a commit node', file_path)
1366 log.debug('Skipping path %s as a commit node', file_path)
1364 continue
1367 continue
1365 yield ArchiveNode(file_path, mode, is_link, repo[fn.hex].read_raw)
1368 yield ArchiveNode(file_path, mode, is_link, repo[fn.hex].read_raw)
1366
1369
1367 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1370 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1368 archive_dir_name, commit_id)
1371 archive_dir_name, commit_id)
@@ -1,888 +1,891 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 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 General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18
18
19 import os
19 import os
20 import subprocess
20 import subprocess
21 from urllib.error import URLError
21 from urllib.error import URLError
22 import urllib.parse
22 import urllib.parse
23 import logging
23 import logging
24 import posixpath as vcspath
24 import posixpath as vcspath
25 import io
25 import io
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 import traceback
29 import traceback
30
30
31 import svn.client
31
32 import svn.core
32 import svn.client # noqa
33 import svn.delta
33 import svn.core # noqa
34 import svn.diff
34 import svn.delta # noqa
35 import svn.fs
35 import svn.diff # noqa
36 import svn.repos
36 import svn.fs # noqa
37 import svn.repos # noqa
37
38
38 from vcsserver import svn_diff, exceptions, subprocessio, settings
39 from vcsserver import svn_diff, exceptions, subprocessio, settings
39 from vcsserver.base import RepoFactory, raise_from_original, ArchiveNode, archive_repo, BinaryEnvelope
40 from vcsserver.base import RepoFactory, raise_from_original, ArchiveNode, archive_repo, BinaryEnvelope
40 from vcsserver.exceptions import NoContentException
41 from vcsserver.exceptions import NoContentException
41 from vcsserver.str_utils import safe_str, safe_bytes
42 from vcsserver.str_utils import safe_str, safe_bytes
42 from vcsserver.vcs_base import RemoteBase
43 from vcsserver.vcs_base import RemoteBase
43 from vcsserver.lib.svnremoterepo import svnremoterepo
44 from vcsserver.lib.svnremoterepo import svnremoterepo
44 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
45
46
46
47
47 svn_compatible_versions_map = {
48 svn_compatible_versions_map = {
48 'pre-1.4-compatible': '1.3',
49 'pre-1.4-compatible': '1.3',
49 'pre-1.5-compatible': '1.4',
50 'pre-1.5-compatible': '1.4',
50 'pre-1.6-compatible': '1.5',
51 'pre-1.6-compatible': '1.5',
51 'pre-1.8-compatible': '1.7',
52 'pre-1.8-compatible': '1.7',
52 'pre-1.9-compatible': '1.8',
53 'pre-1.9-compatible': '1.8',
53 }
54 }
54
55
55 current_compatible_version = '1.14'
56 current_compatible_version = '1.14'
56
57
57
58
58 def reraise_safe_exceptions(func):
59 def reraise_safe_exceptions(func):
59 """Decorator for converting svn exceptions to something neutral."""
60 """Decorator for converting svn exceptions to something neutral."""
60 def wrapper(*args, **kwargs):
61 def wrapper(*args, **kwargs):
61 try:
62 try:
62 return func(*args, **kwargs)
63 return func(*args, **kwargs)
63 except Exception as e:
64 except Exception as e:
64 if not hasattr(e, '_vcs_kind'):
65 if not hasattr(e, '_vcs_kind'):
65 log.exception("Unhandled exception in svn remote call")
66 log.exception("Unhandled exception in svn remote call")
66 raise_from_original(exceptions.UnhandledException(e), e)
67 raise_from_original(exceptions.UnhandledException(e), e)
67 raise
68 raise
68 return wrapper
69 return wrapper
69
70
70
71
71 class SubversionFactory(RepoFactory):
72 class SubversionFactory(RepoFactory):
72 repo_type = 'svn'
73 repo_type = 'svn'
73
74
74 def _create_repo(self, wire, create, compatible_version):
75 def _create_repo(self, wire, create, compatible_version):
75 path = svn.core.svn_path_canonicalize(wire['path'])
76 path = svn.core.svn_path_canonicalize(wire['path'])
76 if create:
77 if create:
77 fs_config = {'compatible-version': current_compatible_version}
78 fs_config = {'compatible-version': current_compatible_version}
78 if compatible_version:
79 if compatible_version:
79
80
80 compatible_version_string = \
81 compatible_version_string = \
81 svn_compatible_versions_map.get(compatible_version) \
82 svn_compatible_versions_map.get(compatible_version) \
82 or compatible_version
83 or compatible_version
83 fs_config['compatible-version'] = compatible_version_string
84 fs_config['compatible-version'] = compatible_version_string
84
85
85 log.debug('Create SVN repo with config "%s"', fs_config)
86 log.debug('Create SVN repo with config "%s"', fs_config)
86 repo = svn.repos.create(path, "", "", None, fs_config)
87 repo = svn.repos.create(path, "", "", None, fs_config)
87 else:
88 else:
88 repo = svn.repos.open(path)
89 repo = svn.repos.open(path)
89
90
90 log.debug('Got SVN object: %s', repo)
91 log.debug('Got SVN object: %s', repo)
91 return repo
92 return repo
92
93
93 def repo(self, wire, create=False, compatible_version=None):
94 def repo(self, wire, create=False, compatible_version=None):
94 """
95 """
95 Get a repository instance for the given path.
96 Get a repository instance for the given path.
96 """
97 """
97 return self._create_repo(wire, create, compatible_version)
98 return self._create_repo(wire, create, compatible_version)
98
99
99
100
100 NODE_TYPE_MAPPING = {
101 NODE_TYPE_MAPPING = {
101 svn.core.svn_node_file: 'file',
102 svn.core.svn_node_file: 'file',
102 svn.core.svn_node_dir: 'dir',
103 svn.core.svn_node_dir: 'dir',
103 }
104 }
104
105
105
106
106 class SvnRemote(RemoteBase):
107 class SvnRemote(RemoteBase):
107
108
108 def __init__(self, factory, hg_factory=None):
109 def __init__(self, factory, hg_factory=None):
109 self._factory = factory
110 self._factory = factory
110
111
111 @reraise_safe_exceptions
112 @reraise_safe_exceptions
112 def discover_svn_version(self):
113 def discover_svn_version(self):
113 try:
114 try:
114 import svn.core
115 import svn.core
115 svn_ver = svn.core.SVN_VERSION
116 svn_ver = svn.core.SVN_VERSION
116 except ImportError:
117 except ImportError:
117 svn_ver = None
118 svn_ver = None
118 return safe_str(svn_ver)
119 return safe_str(svn_ver)
119
120
120 @reraise_safe_exceptions
121 @reraise_safe_exceptions
121 def is_empty(self, wire):
122 def is_empty(self, wire):
122
123
123 try:
124 try:
124 return self.lookup(wire, -1) == 0
125 return self.lookup(wire, -1) == 0
125 except Exception:
126 except Exception:
126 log.exception("failed to read object_store")
127 log.exception("failed to read object_store")
127 return False
128 return False
128
129
129 def check_url(self, url):
130 def check_url(self, url):
130
131
131 # uuid function get's only valid UUID from proper repo, else
132 # uuid function get's only valid UUID from proper repo, else
132 # throws exception
133 # throws exception
133 username, password, src_url = self.get_url_and_credentials(url)
134 username, password, src_url = self.get_url_and_credentials(url)
134 try:
135 try:
135 svnremoterepo(username, password, src_url).svn().uuid
136 svnremoterepo(username, password, src_url).svn().uuid
136 except Exception:
137 except Exception:
137 tb = traceback.format_exc()
138 tb = traceback.format_exc()
138 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
139 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
139 raise URLError(
140 raise URLError(
140 '"%s" is not a valid Subversion source url.' % (url, ))
141 '"%s" is not a valid Subversion source url.' % (url, ))
141 return True
142 return True
142
143
143 def is_path_valid_repository(self, wire, path):
144 def is_path_valid_repository(self, wire, path):
144
145
145 # NOTE(marcink): short circuit the check for SVN repo
146 # NOTE(marcink): short circuit the check for SVN repo
146 # the repos.open might be expensive to check, but we have one cheap
147 # the repos.open might be expensive to check, but we have one cheap
147 # pre condition that we can use, to check for 'format' file
148 # pre condition that we can use, to check for 'format' file
148
149
149 if not os.path.isfile(os.path.join(path, 'format')):
150 if not os.path.isfile(os.path.join(path, 'format')):
150 return False
151 return False
151
152
152 try:
153 try:
153 svn.repos.open(path)
154 svn.repos.open(path)
154 except svn.core.SubversionException:
155 except svn.core.SubversionException:
155 tb = traceback.format_exc()
156 tb = traceback.format_exc()
156 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
157 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
157 return False
158 return False
158 return True
159 return True
159
160
160 @reraise_safe_exceptions
161 @reraise_safe_exceptions
161 def verify(self, wire,):
162 def verify(self, wire,):
162 repo_path = wire['path']
163 repo_path = wire['path']
163 if not self.is_path_valid_repository(wire, repo_path):
164 if not self.is_path_valid_repository(wire, repo_path):
164 raise Exception(
165 raise Exception(
165 "Path %s is not a valid Subversion repository." % repo_path)
166 "Path %s is not a valid Subversion repository." % repo_path)
166
167
167 cmd = ['svnadmin', 'info', repo_path]
168 cmd = ['svnadmin', 'info', repo_path]
168 stdout, stderr = subprocessio.run_command(cmd)
169 stdout, stderr = subprocessio.run_command(cmd)
169 return stdout
170 return stdout
170
171
171 def lookup(self, wire, revision):
172 def lookup(self, wire, revision):
172 if revision not in [-1, None, 'HEAD']:
173 if revision not in [-1, None, 'HEAD']:
173 raise NotImplementedError
174 raise NotImplementedError
174 repo = self._factory.repo(wire)
175 repo = self._factory.repo(wire)
175 fs_ptr = svn.repos.fs(repo)
176 fs_ptr = svn.repos.fs(repo)
176 head = svn.fs.youngest_rev(fs_ptr)
177 head = svn.fs.youngest_rev(fs_ptr)
177 return head
178 return head
178
179
179 def lookup_interval(self, wire, start_ts, end_ts):
180 def lookup_interval(self, wire, start_ts, end_ts):
180 repo = self._factory.repo(wire)
181 repo = self._factory.repo(wire)
181 fsobj = svn.repos.fs(repo)
182 fsobj = svn.repos.fs(repo)
182 start_rev = None
183 start_rev = None
183 end_rev = None
184 end_rev = None
184 if start_ts:
185 if start_ts:
185 start_ts_svn = apr_time_t(start_ts)
186 start_ts_svn = apr_time_t(start_ts)
186 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
187 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
187 else:
188 else:
188 start_rev = 1
189 start_rev = 1
189 if end_ts:
190 if end_ts:
190 end_ts_svn = apr_time_t(end_ts)
191 end_ts_svn = apr_time_t(end_ts)
191 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
192 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
192 else:
193 else:
193 end_rev = svn.fs.youngest_rev(fsobj)
194 end_rev = svn.fs.youngest_rev(fsobj)
194 return start_rev, end_rev
195 return start_rev, end_rev
195
196
196 def revision_properties(self, wire, revision):
197 def revision_properties(self, wire, revision):
197
198
198 cache_on, context_uid, repo_id = self._cache_on(wire)
199 cache_on, context_uid, repo_id = self._cache_on(wire)
199 region = self._region(wire)
200 region = self._region(wire)
200 @region.conditional_cache_on_arguments(condition=cache_on)
201 @region.conditional_cache_on_arguments(condition=cache_on)
201 def _revision_properties(_repo_id, _revision):
202 def _revision_properties(_repo_id, _revision):
202 repo = self._factory.repo(wire)
203 repo = self._factory.repo(wire)
203 fs_ptr = svn.repos.fs(repo)
204 fs_ptr = svn.repos.fs(repo)
204 return svn.fs.revision_proplist(fs_ptr, revision)
205 return svn.fs.revision_proplist(fs_ptr, revision)
205 return _revision_properties(repo_id, revision)
206 return _revision_properties(repo_id, revision)
206
207
207 def revision_changes(self, wire, revision):
208 def revision_changes(self, wire, revision):
208
209
209 repo = self._factory.repo(wire)
210 repo = self._factory.repo(wire)
210 fsobj = svn.repos.fs(repo)
211 fsobj = svn.repos.fs(repo)
211 rev_root = svn.fs.revision_root(fsobj, revision)
212 rev_root = svn.fs.revision_root(fsobj, revision)
212
213
213 editor = svn.repos.ChangeCollector(fsobj, rev_root)
214 editor = svn.repos.ChangeCollector(fsobj, rev_root)
214 editor_ptr, editor_baton = svn.delta.make_editor(editor)
215 editor_ptr, editor_baton = svn.delta.make_editor(editor)
215 base_dir = ""
216 base_dir = ""
216 send_deltas = False
217 send_deltas = False
217 svn.repos.replay2(
218 svn.repos.replay2(
218 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
219 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
219 editor_ptr, editor_baton, None)
220 editor_ptr, editor_baton, None)
220
221
221 added = []
222 added = []
222 changed = []
223 changed = []
223 removed = []
224 removed = []
224
225
225 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
226 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
226 for path, change in editor.changes.items():
227 for path, change in editor.changes.items():
227 # TODO: Decide what to do with directory nodes. Subversion can add
228 # TODO: Decide what to do with directory nodes. Subversion can add
228 # empty directories.
229 # empty directories.
229
230
230 if change.item_kind == svn.core.svn_node_dir:
231 if change.item_kind == svn.core.svn_node_dir:
231 continue
232 continue
232 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
233 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
233 added.append(path)
234 added.append(path)
234 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
235 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
235 svn.repos.CHANGE_ACTION_REPLACE]:
236 svn.repos.CHANGE_ACTION_REPLACE]:
236 changed.append(path)
237 changed.append(path)
237 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
238 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
238 removed.append(path)
239 removed.append(path)
239 else:
240 else:
240 raise NotImplementedError(
241 raise NotImplementedError(
241 "Action %s not supported on path %s" % (
242 "Action %s not supported on path %s" % (
242 change.action, path))
243 change.action, path))
243
244
244 changes = {
245 changes = {
245 'added': added,
246 'added': added,
246 'changed': changed,
247 'changed': changed,
247 'removed': removed,
248 'removed': removed,
248 }
249 }
249 return changes
250 return changes
250
251
251 @reraise_safe_exceptions
252 @reraise_safe_exceptions
252 def node_history(self, wire, path, revision, limit):
253 def node_history(self, wire, path, revision, limit):
253 cache_on, context_uid, repo_id = self._cache_on(wire)
254 cache_on, context_uid, repo_id = self._cache_on(wire)
254 region = self._region(wire)
255 region = self._region(wire)
255 @region.conditional_cache_on_arguments(condition=cache_on)
256 @region.conditional_cache_on_arguments(condition=cache_on)
256 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
257 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
257 cross_copies = False
258 cross_copies = False
258 repo = self._factory.repo(wire)
259 repo = self._factory.repo(wire)
259 fsobj = svn.repos.fs(repo)
260 fsobj = svn.repos.fs(repo)
260 rev_root = svn.fs.revision_root(fsobj, revision)
261 rev_root = svn.fs.revision_root(fsobj, revision)
261
262
262 history_revisions = []
263 history_revisions = []
263 history = svn.fs.node_history(rev_root, path)
264 history = svn.fs.node_history(rev_root, path)
264 history = svn.fs.history_prev(history, cross_copies)
265 history = svn.fs.history_prev(history, cross_copies)
265 while history:
266 while history:
266 __, node_revision = svn.fs.history_location(history)
267 __, node_revision = svn.fs.history_location(history)
267 history_revisions.append(node_revision)
268 history_revisions.append(node_revision)
268 if limit and len(history_revisions) >= limit:
269 if limit and len(history_revisions) >= limit:
269 break
270 break
270 history = svn.fs.history_prev(history, cross_copies)
271 history = svn.fs.history_prev(history, cross_copies)
271 return history_revisions
272 return history_revisions
272 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
273 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
273
274
274 def node_properties(self, wire, path, revision):
275 def node_properties(self, wire, path, revision):
275 cache_on, context_uid, repo_id = self._cache_on(wire)
276 cache_on, context_uid, repo_id = self._cache_on(wire)
276 region = self._region(wire)
277 region = self._region(wire)
278
277 @region.conditional_cache_on_arguments(condition=cache_on)
279 @region.conditional_cache_on_arguments(condition=cache_on)
278 def _node_properties(_repo_id, _path, _revision):
280 def _node_properties(_repo_id, _path, _revision):
279 repo = self._factory.repo(wire)
281 repo = self._factory.repo(wire)
280 fsobj = svn.repos.fs(repo)
282 fsobj = svn.repos.fs(repo)
281 rev_root = svn.fs.revision_root(fsobj, revision)
283 rev_root = svn.fs.revision_root(fsobj, revision)
282 return svn.fs.node_proplist(rev_root, path)
284 return svn.fs.node_proplist(rev_root, path)
283 return _node_properties(repo_id, path, revision)
285 return _node_properties(repo_id, path, revision)
284
286
285 def file_annotate(self, wire, path, revision):
287 def file_annotate(self, wire, path, revision):
286 abs_path = 'file://' + urllib.request.pathname2url(
288 abs_path = 'file://' + urllib.request.pathname2url(
287 vcspath.join(wire['path'], path))
289 vcspath.join(wire['path'], path))
288 file_uri = svn.core.svn_path_canonicalize(abs_path)
290 file_uri = svn.core.svn_path_canonicalize(abs_path)
289
291
290 start_rev = svn_opt_revision_value_t(0)
292 start_rev = svn_opt_revision_value_t(0)
291 peg_rev = svn_opt_revision_value_t(revision)
293 peg_rev = svn_opt_revision_value_t(revision)
292 end_rev = peg_rev
294 end_rev = peg_rev
293
295
294 annotations = []
296 annotations = []
295
297
296 def receiver(line_no, revision, author, date, line, pool):
298 def receiver(line_no, revision, author, date, line, pool):
297 annotations.append((line_no, revision, line))
299 annotations.append((line_no, revision, line))
298
300
299 # TODO: Cannot use blame5, missing typemap function in the swig code
301 # TODO: Cannot use blame5, missing typemap function in the swig code
300 try:
302 try:
301 svn.client.blame2(
303 svn.client.blame2(
302 file_uri, peg_rev, start_rev, end_rev,
304 file_uri, peg_rev, start_rev, end_rev,
303 receiver, svn.client.create_context())
305 receiver, svn.client.create_context())
304 except svn.core.SubversionException as exc:
306 except svn.core.SubversionException as exc:
305 log.exception("Error during blame operation.")
307 log.exception("Error during blame operation.")
306 raise Exception(
308 raise Exception(
307 "Blame not supported or file does not exist at path %s. "
309 "Blame not supported or file does not exist at path %s. "
308 "Error %s." % (path, exc))
310 "Error %s." % (path, exc))
309
311
310 return annotations
312 return annotations
311
313
312 def get_node_type(self, wire, path, revision=None):
314 def get_node_type(self, wire, path, revision=None):
313
315
314 cache_on, context_uid, repo_id = self._cache_on(wire)
316 cache_on, context_uid, repo_id = self._cache_on(wire)
315 region = self._region(wire)
317 region = self._region(wire)
318
316 @region.conditional_cache_on_arguments(condition=cache_on)
319 @region.conditional_cache_on_arguments(condition=cache_on)
317 def _get_node_type(_repo_id, _path, _revision):
320 def _get_node_type(_repo_id, _path, _revision):
318 repo = self._factory.repo(wire)
321 repo = self._factory.repo(wire)
319 fs_ptr = svn.repos.fs(repo)
322 fs_ptr = svn.repos.fs(repo)
320 if _revision is None:
323 if _revision is None:
321 _revision = svn.fs.youngest_rev(fs_ptr)
324 _revision = svn.fs.youngest_rev(fs_ptr)
322 root = svn.fs.revision_root(fs_ptr, _revision)
325 root = svn.fs.revision_root(fs_ptr, _revision)
323 node = svn.fs.check_path(root, path)
326 node = svn.fs.check_path(root, path)
324 return NODE_TYPE_MAPPING.get(node, None)
327 return NODE_TYPE_MAPPING.get(node, None)
325 return _get_node_type(repo_id, path, revision)
328 return _get_node_type(repo_id, path, revision)
326
329
327 def get_nodes(self, wire, path, revision=None):
330 def get_nodes(self, wire, path, revision=None):
328
331
329 cache_on, context_uid, repo_id = self._cache_on(wire)
332 cache_on, context_uid, repo_id = self._cache_on(wire)
330 region = self._region(wire)
333 region = self._region(wire)
331
334
332 @region.conditional_cache_on_arguments(condition=cache_on)
335 @region.conditional_cache_on_arguments(condition=cache_on)
333 def _get_nodes(_repo_id, _path, _revision):
336 def _get_nodes(_repo_id, _path, _revision):
334 repo = self._factory.repo(wire)
337 repo = self._factory.repo(wire)
335 fsobj = svn.repos.fs(repo)
338 fsobj = svn.repos.fs(repo)
336 if _revision is None:
339 if _revision is None:
337 _revision = svn.fs.youngest_rev(fsobj)
340 _revision = svn.fs.youngest_rev(fsobj)
338 root = svn.fs.revision_root(fsobj, _revision)
341 root = svn.fs.revision_root(fsobj, _revision)
339 entries = svn.fs.dir_entries(root, path)
342 entries = svn.fs.dir_entries(root, path)
340 result = []
343 result = []
341 for entry_path, entry_info in entries.items():
344 for entry_path, entry_info in entries.items():
342 result.append(
345 result.append(
343 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
346 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
344 return result
347 return result
345 return _get_nodes(repo_id, path, revision)
348 return _get_nodes(repo_id, path, revision)
346
349
347 def get_file_content(self, wire, path, rev=None):
350 def get_file_content(self, wire, path, rev=None):
348 repo = self._factory.repo(wire)
351 repo = self._factory.repo(wire)
349 fsobj = svn.repos.fs(repo)
352 fsobj = svn.repos.fs(repo)
350 if rev is None:
353 if rev is None:
351 rev = svn.fs.youngest_revision(fsobj)
354 rev = svn.fs.youngest_revision(fsobj)
352 root = svn.fs.revision_root(fsobj, rev)
355 root = svn.fs.revision_root(fsobj, rev)
353 content = svn.core.Stream(svn.fs.file_contents(root, path))
356 content = svn.core.Stream(svn.fs.file_contents(root, path))
354 return BinaryEnvelope(content.read())
357 return BinaryEnvelope(content.read())
355
358
356 def get_file_size(self, wire, path, revision=None):
359 def get_file_size(self, wire, path, revision=None):
357
360
358 cache_on, context_uid, repo_id = self._cache_on(wire)
361 cache_on, context_uid, repo_id = self._cache_on(wire)
359 region = self._region(wire)
362 region = self._region(wire)
360
363
361 @region.conditional_cache_on_arguments(condition=cache_on)
364 @region.conditional_cache_on_arguments(condition=cache_on)
362 def _get_file_size(_repo_id, _path, _revision):
365 def _get_file_size(_repo_id, _path, _revision):
363 repo = self._factory.repo(wire)
366 repo = self._factory.repo(wire)
364 fsobj = svn.repos.fs(repo)
367 fsobj = svn.repos.fs(repo)
365 if _revision is None:
368 if _revision is None:
366 _revision = svn.fs.youngest_revision(fsobj)
369 _revision = svn.fs.youngest_revision(fsobj)
367 root = svn.fs.revision_root(fsobj, _revision)
370 root = svn.fs.revision_root(fsobj, _revision)
368 size = svn.fs.file_length(root, path)
371 size = svn.fs.file_length(root, path)
369 return size
372 return size
370 return _get_file_size(repo_id, path, revision)
373 return _get_file_size(repo_id, path, revision)
371
374
372 def create_repository(self, wire, compatible_version=None):
375 def create_repository(self, wire, compatible_version=None):
373 log.info('Creating Subversion repository in path "%s"', wire['path'])
376 log.info('Creating Subversion repository in path "%s"', wire['path'])
374 self._factory.repo(wire, create=True,
377 self._factory.repo(wire, create=True,
375 compatible_version=compatible_version)
378 compatible_version=compatible_version)
376
379
377 def get_url_and_credentials(self, src_url):
380 def get_url_and_credentials(self, src_url):
378 obj = urllib.parse.urlparse(src_url)
381 obj = urllib.parse.urlparse(src_url)
379 username = obj.username or None
382 username = obj.username or None
380 password = obj.password or None
383 password = obj.password or None
381 return username, password, src_url
384 return username, password, src_url
382
385
383 def import_remote_repository(self, wire, src_url):
386 def import_remote_repository(self, wire, src_url):
384 repo_path = wire['path']
387 repo_path = wire['path']
385 if not self.is_path_valid_repository(wire, repo_path):
388 if not self.is_path_valid_repository(wire, repo_path):
386 raise Exception(
389 raise Exception(
387 "Path %s is not a valid Subversion repository." % repo_path)
390 "Path %s is not a valid Subversion repository." % repo_path)
388
391
389 username, password, src_url = self.get_url_and_credentials(src_url)
392 username, password, src_url = self.get_url_and_credentials(src_url)
390 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
393 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
391 '--trust-server-cert-failures=unknown-ca']
394 '--trust-server-cert-failures=unknown-ca']
392 if username and password:
395 if username and password:
393 rdump_cmd += ['--username', username, '--password', password]
396 rdump_cmd += ['--username', username, '--password', password]
394 rdump_cmd += [src_url]
397 rdump_cmd += [src_url]
395
398
396 rdump = subprocess.Popen(
399 rdump = subprocess.Popen(
397 rdump_cmd,
400 rdump_cmd,
398 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
401 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
399 load = subprocess.Popen(
402 load = subprocess.Popen(
400 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
403 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
401
404
402 # TODO: johbo: This can be a very long operation, might be better
405 # TODO: johbo: This can be a very long operation, might be better
403 # to track some kind of status and provide an api to check if the
406 # to track some kind of status and provide an api to check if the
404 # import is done.
407 # import is done.
405 rdump.wait()
408 rdump.wait()
406 load.wait()
409 load.wait()
407
410
408 log.debug('Return process ended with code: %s', rdump.returncode)
411 log.debug('Return process ended with code: %s', rdump.returncode)
409 if rdump.returncode != 0:
412 if rdump.returncode != 0:
410 errors = rdump.stderr.read()
413 errors = rdump.stderr.read()
411 log.error('svnrdump dump failed: statuscode %s: message: %s', rdump.returncode, errors)
414 log.error('svnrdump dump failed: statuscode %s: message: %s', rdump.returncode, errors)
412
415
413 reason = 'UNKNOWN'
416 reason = 'UNKNOWN'
414 if b'svnrdump: E230001:' in errors:
417 if b'svnrdump: E230001:' in errors:
415 reason = 'INVALID_CERTIFICATE'
418 reason = 'INVALID_CERTIFICATE'
416
419
417 if reason == 'UNKNOWN':
420 if reason == 'UNKNOWN':
418 reason = 'UNKNOWN:{}'.format(safe_str(errors))
421 reason = 'UNKNOWN:{}'.format(safe_str(errors))
419
422
420 raise Exception(
423 raise Exception(
421 'Failed to dump the remote repository from %s. Reason:%s' % (
424 'Failed to dump the remote repository from %s. Reason:%s' % (
422 src_url, reason))
425 src_url, reason))
423 if load.returncode != 0:
426 if load.returncode != 0:
424 raise Exception(
427 raise Exception(
425 'Failed to load the dump of remote repository from %s.' %
428 'Failed to load the dump of remote repository from %s.' %
426 (src_url, ))
429 (src_url, ))
427
430
428 def commit(self, wire, message, author, timestamp, updated, removed):
431 def commit(self, wire, message, author, timestamp, updated, removed):
429
432
430 updated = [{k: safe_bytes(v) for k, v in x.items() if isinstance(v, str)} for x in updated]
433 updated = [{k: safe_bytes(v) for k, v in x.items() if isinstance(v, str)} for x in updated]
431
434
432 message = safe_bytes(message)
435 message = safe_bytes(message)
433 author = safe_bytes(author)
436 author = safe_bytes(author)
434
437
435 repo = self._factory.repo(wire)
438 repo = self._factory.repo(wire)
436 fsobj = svn.repos.fs(repo)
439 fsobj = svn.repos.fs(repo)
437
440
438 rev = svn.fs.youngest_rev(fsobj)
441 rev = svn.fs.youngest_rev(fsobj)
439 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
442 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
440 txn_root = svn.fs.txn_root(txn)
443 txn_root = svn.fs.txn_root(txn)
441
444
442 for node in updated:
445 for node in updated:
443 TxnNodeProcessor(node, txn_root).update()
446 TxnNodeProcessor(node, txn_root).update()
444 for node in removed:
447 for node in removed:
445 TxnNodeProcessor(node, txn_root).remove()
448 TxnNodeProcessor(node, txn_root).remove()
446
449
447 commit_id = svn.repos.fs_commit_txn(repo, txn)
450 commit_id = svn.repos.fs_commit_txn(repo, txn)
448
451
449 if timestamp:
452 if timestamp:
450 apr_time = int(apr_time_t(timestamp))
453 apr_time = int(apr_time_t(timestamp))
451 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
454 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
452 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
455 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
453
456
454 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
457 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
455 return commit_id
458 return commit_id
456
459
457 def diff(self, wire, rev1, rev2, path1=None, path2=None,
460 def diff(self, wire, rev1, rev2, path1=None, path2=None,
458 ignore_whitespace=False, context=3):
461 ignore_whitespace=False, context=3):
459
462
460 wire.update(cache=False)
463 wire.update(cache=False)
461 repo = self._factory.repo(wire)
464 repo = self._factory.repo(wire)
462 diff_creator = SvnDiffer(
465 diff_creator = SvnDiffer(
463 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
466 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
464 try:
467 try:
465 return BinaryEnvelope(diff_creator.generate_diff())
468 return BinaryEnvelope(diff_creator.generate_diff())
466 except svn.core.SubversionException as e:
469 except svn.core.SubversionException as e:
467 log.exception(
470 log.exception(
468 "Error during diff operation operation. "
471 "Error during diff operation operation. "
469 "Path might not exist %s, %s", path1, path2)
472 "Path might not exist %s, %s", path1, path2)
470 return BinaryEnvelope(b'')
473 return BinaryEnvelope(b'')
471
474
472 @reraise_safe_exceptions
475 @reraise_safe_exceptions
473 def is_large_file(self, wire, path):
476 def is_large_file(self, wire, path):
474 return False
477 return False
475
478
476 @reraise_safe_exceptions
479 @reraise_safe_exceptions
477 def is_binary(self, wire, rev, path):
480 def is_binary(self, wire, rev, path):
478 cache_on, context_uid, repo_id = self._cache_on(wire)
481 cache_on, context_uid, repo_id = self._cache_on(wire)
479 region = self._region(wire)
482 region = self._region(wire)
480
483
481 @region.conditional_cache_on_arguments(condition=cache_on)
484 @region.conditional_cache_on_arguments(condition=cache_on)
482 def _is_binary(_repo_id, _rev, _path):
485 def _is_binary(_repo_id, _rev, _path):
483 raw_bytes = self.get_file_content(wire, path, rev)
486 raw_bytes = self.get_file_content(wire, path, rev)
484 return raw_bytes and b'\0' in raw_bytes
487 return raw_bytes and b'\0' in raw_bytes
485
488
486 return _is_binary(repo_id, rev, path)
489 return _is_binary(repo_id, rev, path)
487
490
488 @reraise_safe_exceptions
491 @reraise_safe_exceptions
489 def md5_hash(self, wire, rev, path):
492 def md5_hash(self, wire, rev, path):
490 cache_on, context_uid, repo_id = self._cache_on(wire)
493 cache_on, context_uid, repo_id = self._cache_on(wire)
491 region = self._region(wire)
494 region = self._region(wire)
492
495
493 @region.conditional_cache_on_arguments(condition=cache_on)
496 @region.conditional_cache_on_arguments(condition=cache_on)
494 def _md5_hash(_repo_id, _rev, _path):
497 def _md5_hash(_repo_id, _rev, _path):
495 return ''
498 return ''
496
499
497 return _md5_hash(repo_id, rev, path)
500 return _md5_hash(repo_id, rev, path)
498
501
499 @reraise_safe_exceptions
502 @reraise_safe_exceptions
500 def run_svn_command(self, wire, cmd, **opts):
503 def run_svn_command(self, wire, cmd, **opts):
501 path = wire.get('path', None)
504 path = wire.get('path', None)
502
505
503 if path and os.path.isdir(path):
506 if path and os.path.isdir(path):
504 opts['cwd'] = path
507 opts['cwd'] = path
505
508
506 safe_call = opts.pop('_safe', False)
509 safe_call = opts.pop('_safe', False)
507
510
508 svnenv = os.environ.copy()
511 svnenv = os.environ.copy()
509 svnenv.update(opts.pop('extra_env', {}))
512 svnenv.update(opts.pop('extra_env', {}))
510
513
511 _opts = {'env': svnenv, 'shell': False}
514 _opts = {'env': svnenv, 'shell': False}
512
515
513 try:
516 try:
514 _opts.update(opts)
517 _opts.update(opts)
515 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
518 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
516
519
517 return b''.join(proc), b''.join(proc.stderr)
520 return b''.join(proc), b''.join(proc.stderr)
518 except OSError as err:
521 except OSError as err:
519 if safe_call:
522 if safe_call:
520 return '', safe_str(err).strip()
523 return '', safe_str(err).strip()
521 else:
524 else:
522 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
525 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
523 tb_err = ("Couldn't run svn command (%s).\n"
526 tb_err = ("Couldn't run svn command (%s).\n"
524 "Original error was:%s\n"
527 "Original error was:%s\n"
525 "Call options:%s\n"
528 "Call options:%s\n"
526 % (cmd, err, _opts))
529 % (cmd, err, _opts))
527 log.exception(tb_err)
530 log.exception(tb_err)
528 raise exceptions.VcsException()(tb_err)
531 raise exceptions.VcsException()(tb_err)
529
532
530 @reraise_safe_exceptions
533 @reraise_safe_exceptions
531 def install_hooks(self, wire, force=False):
534 def install_hooks(self, wire, force=False):
532 from vcsserver.hook_utils import install_svn_hooks
535 from vcsserver.hook_utils import install_svn_hooks
533 repo_path = wire['path']
536 repo_path = wire['path']
534 binary_dir = settings.BINARY_DIR
537 binary_dir = settings.BINARY_DIR
535 executable = None
538 executable = None
536 if binary_dir:
539 if binary_dir:
537 executable = os.path.join(binary_dir, 'python3')
540 executable = os.path.join(binary_dir, 'python3')
538 return install_svn_hooks(repo_path, force_create=force)
541 return install_svn_hooks(repo_path, force_create=force)
539
542
540 @reraise_safe_exceptions
543 @reraise_safe_exceptions
541 def get_hooks_info(self, wire):
544 def get_hooks_info(self, wire):
542 from vcsserver.hook_utils import (
545 from vcsserver.hook_utils import (
543 get_svn_pre_hook_version, get_svn_post_hook_version)
546 get_svn_pre_hook_version, get_svn_post_hook_version)
544 repo_path = wire['path']
547 repo_path = wire['path']
545 return {
548 return {
546 'pre_version': get_svn_pre_hook_version(repo_path),
549 'pre_version': get_svn_pre_hook_version(repo_path),
547 'post_version': get_svn_post_hook_version(repo_path),
550 'post_version': get_svn_post_hook_version(repo_path),
548 }
551 }
549
552
550 @reraise_safe_exceptions
553 @reraise_safe_exceptions
551 def set_head_ref(self, wire, head_name):
554 def set_head_ref(self, wire, head_name):
552 pass
555 pass
553
556
554 @reraise_safe_exceptions
557 @reraise_safe_exceptions
555 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
558 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
556 archive_dir_name, commit_id):
559 archive_dir_name, commit_id):
557
560
558 def walk_tree(root, root_dir, _commit_id):
561 def walk_tree(root, root_dir, _commit_id):
559 """
562 """
560 Special recursive svn repo walker
563 Special recursive svn repo walker
561 """
564 """
562 root_dir = safe_bytes(root_dir)
565 root_dir = safe_bytes(root_dir)
563
566
564 filemode_default = 0o100644
567 filemode_default = 0o100644
565 filemode_executable = 0o100755
568 filemode_executable = 0o100755
566
569
567 file_iter = svn.fs.dir_entries(root, root_dir)
570 file_iter = svn.fs.dir_entries(root, root_dir)
568 for f_name in file_iter:
571 for f_name in file_iter:
569 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
572 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
570
573
571 if f_type == 'dir':
574 if f_type == 'dir':
572 # return only DIR, and then all entries in that dir
575 # return only DIR, and then all entries in that dir
573 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
576 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
574 new_root = os.path.join(root_dir, f_name)
577 new_root = os.path.join(root_dir, f_name)
575 for _f_name, _f_data, _f_type in walk_tree(root, new_root, _commit_id):
578 for _f_name, _f_data, _f_type in walk_tree(root, new_root, _commit_id):
576 yield _f_name, _f_data, _f_type
579 yield _f_name, _f_data, _f_type
577 else:
580 else:
578
581
579 f_path = os.path.join(root_dir, f_name).rstrip(b'/')
582 f_path = os.path.join(root_dir, f_name).rstrip(b'/')
580 prop_list = svn.fs.node_proplist(root, f_path)
583 prop_list = svn.fs.node_proplist(root, f_path)
581
584
582 f_mode = filemode_default
585 f_mode = filemode_default
583 if prop_list.get('svn:executable'):
586 if prop_list.get('svn:executable'):
584 f_mode = filemode_executable
587 f_mode = filemode_executable
585
588
586 f_is_link = False
589 f_is_link = False
587 if prop_list.get('svn:special'):
590 if prop_list.get('svn:special'):
588 f_is_link = True
591 f_is_link = True
589
592
590 data = {
593 data = {
591 'is_link': f_is_link,
594 'is_link': f_is_link,
592 'mode': f_mode,
595 'mode': f_mode,
593 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
596 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
594 }
597 }
595
598
596 yield f_path, data, f_type
599 yield f_path, data, f_type
597
600
598 def file_walker(_commit_id, path):
601 def file_walker(_commit_id, path):
599 repo = self._factory.repo(wire)
602 repo = self._factory.repo(wire)
600 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
603 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
601
604
602 def no_content():
605 def no_content():
603 raise NoContentException()
606 raise NoContentException()
604
607
605 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
608 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
606 file_path = f_name
609 file_path = f_name
607
610
608 if f_type == 'dir':
611 if f_type == 'dir':
609 mode = f_data['mode']
612 mode = f_data['mode']
610 yield ArchiveNode(file_path, mode, False, no_content)
613 yield ArchiveNode(file_path, mode, False, no_content)
611 else:
614 else:
612 mode = f_data['mode']
615 mode = f_data['mode']
613 is_link = f_data['is_link']
616 is_link = f_data['is_link']
614 data_stream = f_data['content_stream']
617 data_stream = f_data['content_stream']
615 yield ArchiveNode(file_path, mode, is_link, data_stream)
618 yield ArchiveNode(file_path, mode, is_link, data_stream)
616
619
617 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
620 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
618 archive_dir_name, commit_id)
621 archive_dir_name, commit_id)
619
622
620
623
621 class SvnDiffer(object):
624 class SvnDiffer(object):
622 """
625 """
623 Utility to create diffs based on difflib and the Subversion api
626 Utility to create diffs based on difflib and the Subversion api
624 """
627 """
625
628
626 binary_content = False
629 binary_content = False
627
630
628 def __init__(
631 def __init__(
629 self, repo, src_rev, src_path, tgt_rev, tgt_path,
632 self, repo, src_rev, src_path, tgt_rev, tgt_path,
630 ignore_whitespace, context):
633 ignore_whitespace, context):
631 self.repo = repo
634 self.repo = repo
632 self.ignore_whitespace = ignore_whitespace
635 self.ignore_whitespace = ignore_whitespace
633 self.context = context
636 self.context = context
634
637
635 fsobj = svn.repos.fs(repo)
638 fsobj = svn.repos.fs(repo)
636
639
637 self.tgt_rev = tgt_rev
640 self.tgt_rev = tgt_rev
638 self.tgt_path = tgt_path or ''
641 self.tgt_path = tgt_path or ''
639 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
642 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
640 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
643 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
641
644
642 self.src_rev = src_rev
645 self.src_rev = src_rev
643 self.src_path = src_path or self.tgt_path
646 self.src_path = src_path or self.tgt_path
644 self.src_root = svn.fs.revision_root(fsobj, src_rev)
647 self.src_root = svn.fs.revision_root(fsobj, src_rev)
645 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
648 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
646
649
647 self._validate()
650 self._validate()
648
651
649 def _validate(self):
652 def _validate(self):
650 if (self.tgt_kind != svn.core.svn_node_none and
653 if (self.tgt_kind != svn.core.svn_node_none and
651 self.src_kind != svn.core.svn_node_none and
654 self.src_kind != svn.core.svn_node_none and
652 self.src_kind != self.tgt_kind):
655 self.src_kind != self.tgt_kind):
653 # TODO: johbo: proper error handling
656 # TODO: johbo: proper error handling
654 raise Exception(
657 raise Exception(
655 "Source and target are not compatible for diff generation. "
658 "Source and target are not compatible for diff generation. "
656 "Source type: %s, target type: %s" %
659 "Source type: %s, target type: %s" %
657 (self.src_kind, self.tgt_kind))
660 (self.src_kind, self.tgt_kind))
658
661
659 def generate_diff(self):
662 def generate_diff(self):
660 buf = io.StringIO()
663 buf = io.StringIO()
661 if self.tgt_kind == svn.core.svn_node_dir:
664 if self.tgt_kind == svn.core.svn_node_dir:
662 self._generate_dir_diff(buf)
665 self._generate_dir_diff(buf)
663 else:
666 else:
664 self._generate_file_diff(buf)
667 self._generate_file_diff(buf)
665 return buf.getvalue()
668 return buf.getvalue()
666
669
667 def _generate_dir_diff(self, buf):
670 def _generate_dir_diff(self, buf):
668 editor = DiffChangeEditor()
671 editor = DiffChangeEditor()
669 editor_ptr, editor_baton = svn.delta.make_editor(editor)
672 editor_ptr, editor_baton = svn.delta.make_editor(editor)
670 svn.repos.dir_delta2(
673 svn.repos.dir_delta2(
671 self.src_root,
674 self.src_root,
672 self.src_path,
675 self.src_path,
673 '', # src_entry
676 '', # src_entry
674 self.tgt_root,
677 self.tgt_root,
675 self.tgt_path,
678 self.tgt_path,
676 editor_ptr, editor_baton,
679 editor_ptr, editor_baton,
677 authorization_callback_allow_all,
680 authorization_callback_allow_all,
678 False, # text_deltas
681 False, # text_deltas
679 svn.core.svn_depth_infinity, # depth
682 svn.core.svn_depth_infinity, # depth
680 False, # entry_props
683 False, # entry_props
681 False, # ignore_ancestry
684 False, # ignore_ancestry
682 )
685 )
683
686
684 for path, __, change in sorted(editor.changes):
687 for path, __, change in sorted(editor.changes):
685 self._generate_node_diff(
688 self._generate_node_diff(
686 buf, change, path, self.tgt_path, path, self.src_path)
689 buf, change, path, self.tgt_path, path, self.src_path)
687
690
688 def _generate_file_diff(self, buf):
691 def _generate_file_diff(self, buf):
689 change = None
692 change = None
690 if self.src_kind == svn.core.svn_node_none:
693 if self.src_kind == svn.core.svn_node_none:
691 change = "add"
694 change = "add"
692 elif self.tgt_kind == svn.core.svn_node_none:
695 elif self.tgt_kind == svn.core.svn_node_none:
693 change = "delete"
696 change = "delete"
694 tgt_base, tgt_path = vcspath.split(self.tgt_path)
697 tgt_base, tgt_path = vcspath.split(self.tgt_path)
695 src_base, src_path = vcspath.split(self.src_path)
698 src_base, src_path = vcspath.split(self.src_path)
696 self._generate_node_diff(
699 self._generate_node_diff(
697 buf, change, tgt_path, tgt_base, src_path, src_base)
700 buf, change, tgt_path, tgt_base, src_path, src_base)
698
701
699 def _generate_node_diff(
702 def _generate_node_diff(
700 self, buf, change, tgt_path, tgt_base, src_path, src_base):
703 self, buf, change, tgt_path, tgt_base, src_path, src_base):
701
704
702
705
703 tgt_path = safe_str(tgt_path)
706 tgt_path = safe_str(tgt_path)
704 src_path = safe_str(src_path)
707 src_path = safe_str(src_path)
705
708
706
709
707 if self.src_rev == self.tgt_rev and tgt_base == src_base:
710 if self.src_rev == self.tgt_rev and tgt_base == src_base:
708 # makes consistent behaviour with git/hg to return empty diff if
711 # makes consistent behaviour with git/hg to return empty diff if
709 # we compare same revisions
712 # we compare same revisions
710 return
713 return
711
714
712 tgt_full_path = vcspath.join(tgt_base, tgt_path)
715 tgt_full_path = vcspath.join(tgt_base, tgt_path)
713 src_full_path = vcspath.join(src_base, src_path)
716 src_full_path = vcspath.join(src_base, src_path)
714
717
715 self.binary_content = False
718 self.binary_content = False
716 mime_type = self._get_mime_type(tgt_full_path)
719 mime_type = self._get_mime_type(tgt_full_path)
717
720
718 if mime_type and not mime_type.startswith('text'):
721 if mime_type and not mime_type.startswith('text'):
719 self.binary_content = True
722 self.binary_content = True
720 buf.write("=" * 67 + '\n')
723 buf.write("=" * 67 + '\n')
721 buf.write("Cannot display: file marked as a binary type.\n")
724 buf.write("Cannot display: file marked as a binary type.\n")
722 buf.write("svn:mime-type = %s\n" % mime_type)
725 buf.write("svn:mime-type = %s\n" % mime_type)
723 buf.write("Index: %s\n" % (tgt_path, ))
726 buf.write("Index: %s\n" % (tgt_path, ))
724 buf.write("=" * 67 + '\n')
727 buf.write("=" * 67 + '\n')
725 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
728 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
726 'tgt_path': tgt_path})
729 'tgt_path': tgt_path})
727
730
728 if change == 'add':
731 if change == 'add':
729 # TODO: johbo: SVN is missing a zero here compared to git
732 # TODO: johbo: SVN is missing a zero here compared to git
730 buf.write("new file mode 10644\n")
733 buf.write("new file mode 10644\n")
731
734
732 #TODO(marcink): intro to binary detection of svn patches
735 #TODO(marcink): intro to binary detection of svn patches
733 # if self.binary_content:
736 # if self.binary_content:
734 # buf.write('GIT binary patch\n')
737 # buf.write('GIT binary patch\n')
735
738
736 buf.write("--- /dev/null\t(revision 0)\n")
739 buf.write("--- /dev/null\t(revision 0)\n")
737 src_lines = []
740 src_lines = []
738 else:
741 else:
739 if change == 'delete':
742 if change == 'delete':
740 buf.write("deleted file mode 10644\n")
743 buf.write("deleted file mode 10644\n")
741
744
742 #TODO(marcink): intro to binary detection of svn patches
745 #TODO(marcink): intro to binary detection of svn patches
743 # if self.binary_content:
746 # if self.binary_content:
744 # buf.write('GIT binary patch\n')
747 # buf.write('GIT binary patch\n')
745
748
746 buf.write("--- a/%s\t(revision %s)\n" % (
749 buf.write("--- a/%s\t(revision %s)\n" % (
747 src_path, self.src_rev))
750 src_path, self.src_rev))
748 src_lines = self._svn_readlines(self.src_root, src_full_path)
751 src_lines = self._svn_readlines(self.src_root, src_full_path)
749
752
750 if change == 'delete':
753 if change == 'delete':
751 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
754 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
752 tgt_lines = []
755 tgt_lines = []
753 else:
756 else:
754 buf.write("+++ b/%s\t(revision %s)\n" % (
757 buf.write("+++ b/%s\t(revision %s)\n" % (
755 tgt_path, self.tgt_rev))
758 tgt_path, self.tgt_rev))
756 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
759 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
757
760
758 if not self.binary_content:
761 if not self.binary_content:
759 udiff = svn_diff.unified_diff(
762 udiff = svn_diff.unified_diff(
760 src_lines, tgt_lines, context=self.context,
763 src_lines, tgt_lines, context=self.context,
761 ignore_blank_lines=self.ignore_whitespace,
764 ignore_blank_lines=self.ignore_whitespace,
762 ignore_case=False,
765 ignore_case=False,
763 ignore_space_changes=self.ignore_whitespace)
766 ignore_space_changes=self.ignore_whitespace)
764
767
765 buf.writelines(udiff)
768 buf.writelines(udiff)
766
769
767 def _get_mime_type(self, path):
770 def _get_mime_type(self, path):
768 try:
771 try:
769 mime_type = svn.fs.node_prop(
772 mime_type = svn.fs.node_prop(
770 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
773 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
771 except svn.core.SubversionException:
774 except svn.core.SubversionException:
772 mime_type = svn.fs.node_prop(
775 mime_type = svn.fs.node_prop(
773 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
776 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
774 return mime_type
777 return mime_type
775
778
776 def _svn_readlines(self, fs_root, node_path):
779 def _svn_readlines(self, fs_root, node_path):
777 if self.binary_content:
780 if self.binary_content:
778 return []
781 return []
779 node_kind = svn.fs.check_path(fs_root, node_path)
782 node_kind = svn.fs.check_path(fs_root, node_path)
780 if node_kind not in (
783 if node_kind not in (
781 svn.core.svn_node_file, svn.core.svn_node_symlink):
784 svn.core.svn_node_file, svn.core.svn_node_symlink):
782 return []
785 return []
783 content = svn.core.Stream(
786 content = svn.core.Stream(
784 svn.fs.file_contents(fs_root, node_path)).read()
787 svn.fs.file_contents(fs_root, node_path)).read()
785
788
786 return content.splitlines(True)
789 return content.splitlines(True)
787
790
788
791
789 class DiffChangeEditor(svn.delta.Editor):
792 class DiffChangeEditor(svn.delta.Editor):
790 """
793 """
791 Records changes between two given revisions
794 Records changes between two given revisions
792 """
795 """
793
796
794 def __init__(self):
797 def __init__(self):
795 self.changes = []
798 self.changes = []
796
799
797 def delete_entry(self, path, revision, parent_baton, pool=None):
800 def delete_entry(self, path, revision, parent_baton, pool=None):
798 self.changes.append((path, None, 'delete'))
801 self.changes.append((path, None, 'delete'))
799
802
800 def add_file(
803 def add_file(
801 self, path, parent_baton, copyfrom_path, copyfrom_revision,
804 self, path, parent_baton, copyfrom_path, copyfrom_revision,
802 file_pool=None):
805 file_pool=None):
803 self.changes.append((path, 'file', 'add'))
806 self.changes.append((path, 'file', 'add'))
804
807
805 def open_file(self, path, parent_baton, base_revision, file_pool=None):
808 def open_file(self, path, parent_baton, base_revision, file_pool=None):
806 self.changes.append((path, 'file', 'change'))
809 self.changes.append((path, 'file', 'change'))
807
810
808
811
809 def authorization_callback_allow_all(root, path, pool):
812 def authorization_callback_allow_all(root, path, pool):
810 return True
813 return True
811
814
812
815
813 class TxnNodeProcessor(object):
816 class TxnNodeProcessor(object):
814 """
817 """
815 Utility to process the change of one node within a transaction root.
818 Utility to process the change of one node within a transaction root.
816
819
817 It encapsulates the knowledge of how to add, update or remove
820 It encapsulates the knowledge of how to add, update or remove
818 a node for a given transaction root. The purpose is to support the method
821 a node for a given transaction root. The purpose is to support the method
819 `SvnRemote.commit`.
822 `SvnRemote.commit`.
820 """
823 """
821
824
822 def __init__(self, node, txn_root):
825 def __init__(self, node, txn_root):
823 assert isinstance(node['path'], bytes)
826 assert isinstance(node['path'], bytes)
824
827
825 self.node = node
828 self.node = node
826 self.txn_root = txn_root
829 self.txn_root = txn_root
827
830
828 def update(self):
831 def update(self):
829 self._ensure_parent_dirs()
832 self._ensure_parent_dirs()
830 self._add_file_if_node_does_not_exist()
833 self._add_file_if_node_does_not_exist()
831 self._update_file_content()
834 self._update_file_content()
832 self._update_file_properties()
835 self._update_file_properties()
833
836
834 def remove(self):
837 def remove(self):
835 svn.fs.delete(self.txn_root, self.node['path'])
838 svn.fs.delete(self.txn_root, self.node['path'])
836 # TODO: Clean up directory if empty
839 # TODO: Clean up directory if empty
837
840
838 def _ensure_parent_dirs(self):
841 def _ensure_parent_dirs(self):
839 curdir = vcspath.dirname(self.node['path'])
842 curdir = vcspath.dirname(self.node['path'])
840 dirs_to_create = []
843 dirs_to_create = []
841 while not self._svn_path_exists(curdir):
844 while not self._svn_path_exists(curdir):
842 dirs_to_create.append(curdir)
845 dirs_to_create.append(curdir)
843 curdir = vcspath.dirname(curdir)
846 curdir = vcspath.dirname(curdir)
844
847
845 for curdir in reversed(dirs_to_create):
848 for curdir in reversed(dirs_to_create):
846 log.debug('Creating missing directory "%s"', curdir)
849 log.debug('Creating missing directory "%s"', curdir)
847 svn.fs.make_dir(self.txn_root, curdir)
850 svn.fs.make_dir(self.txn_root, curdir)
848
851
849 def _svn_path_exists(self, path):
852 def _svn_path_exists(self, path):
850 path_status = svn.fs.check_path(self.txn_root, path)
853 path_status = svn.fs.check_path(self.txn_root, path)
851 return path_status != svn.core.svn_node_none
854 return path_status != svn.core.svn_node_none
852
855
853 def _add_file_if_node_does_not_exist(self):
856 def _add_file_if_node_does_not_exist(self):
854 kind = svn.fs.check_path(self.txn_root, self.node['path'])
857 kind = svn.fs.check_path(self.txn_root, self.node['path'])
855 if kind == svn.core.svn_node_none:
858 if kind == svn.core.svn_node_none:
856 svn.fs.make_file(self.txn_root, self.node['path'])
859 svn.fs.make_file(self.txn_root, self.node['path'])
857
860
858 def _update_file_content(self):
861 def _update_file_content(self):
859 assert isinstance(self.node['content'], bytes)
862 assert isinstance(self.node['content'], bytes)
860
863
861 handler, baton = svn.fs.apply_textdelta(
864 handler, baton = svn.fs.apply_textdelta(
862 self.txn_root, self.node['path'], None, None)
865 self.txn_root, self.node['path'], None, None)
863 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
866 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
864
867
865 def _update_file_properties(self):
868 def _update_file_properties(self):
866 properties = self.node.get('properties', {})
869 properties = self.node.get('properties', {})
867 for key, value in properties.items():
870 for key, value in properties.items():
868 svn.fs.change_node_prop(
871 svn.fs.change_node_prop(
869 self.txn_root, self.node['path'], key, value)
872 self.txn_root, self.node['path'], key, value)
870
873
871
874
872 def apr_time_t(timestamp):
875 def apr_time_t(timestamp):
873 """
876 """
874 Convert a Python timestamp into APR timestamp type apr_time_t
877 Convert a Python timestamp into APR timestamp type apr_time_t
875 """
878 """
876 return timestamp * 1E6
879 return timestamp * 1E6
877
880
878
881
879 def svn_opt_revision_value_t(num):
882 def svn_opt_revision_value_t(num):
880 """
883 """
881 Put `num` into a `svn_opt_revision_value_t` structure.
884 Put `num` into a `svn_opt_revision_value_t` structure.
882 """
885 """
883 value = svn.core.svn_opt_revision_value_t()
886 value = svn.core.svn_opt_revision_value_t()
884 value.number = num
887 value.number = num
885 revision = svn.core.svn_opt_revision_t()
888 revision = svn.core.svn_opt_revision_t()
886 revision.kind = svn.core.svn_opt_revision_number
889 revision.kind = svn.core.svn_opt_revision_number
887 revision.value = value
890 revision.value = value
888 return revision
891 return revision
General Comments 0
You need to be logged in to leave comments. Login now