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