##// END OF EJS Templates
python3: code change for py3 support...
super-admin -
r1048:742e21ae python3
parent child Browse files
Show More
@@ -0,0 +1,1 b''
1 import simplejson as json
@@ -0,0 +1,53 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
18 import pytest
19 from vcsserver.utils import ascii_bytes, ascii_str
20
21
22 @pytest.mark.parametrize('given, expected', [
23 ('a', b'a'),
24 (u'a', b'a'),
25 ])
26 def test_ascii_bytes(given, expected):
27 assert ascii_bytes(given) == expected
28
29
30 @pytest.mark.parametrize('given', [
31 'Ã¥',
32 'Ã¥'.encode('utf8')
33 ])
34 def test_ascii_bytes_raises(given):
35 with pytest.raises(ValueError):
36 ascii_bytes(given)
37
38
39 @pytest.mark.parametrize('given, expected', [
40 (b'a', 'a'),
41 ])
42 def test_ascii_str(given, expected):
43 assert ascii_str(given) == expected
44
45
46 @pytest.mark.parametrize('given', [
47 u'a',
48 'Ã¥'.encode('utf8'),
49 u'Ã¥'
50 ])
51 def test_ascii_str_raises(given):
52 with pytest.raises(ValueError):
53 ascii_str(given)
@@ -1,132 +1,134 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 import os
17 import os
18 import sys
18 import sys
19 import traceback
19 import traceback
20 import logging
20 import logging
21 import urllib.parse
21 import urllib.parse
22
22
23 from vcsserver.lib.rc_cache import region_meta
23 from vcsserver.lib.rc_cache import region_meta
24
24
25 from vcsserver import exceptions
25 from vcsserver import exceptions
26 from vcsserver.exceptions import NoContentException
26 from vcsserver.exceptions import NoContentException
27 from vcsserver.hgcompat import (archival)
27 from vcsserver.hgcompat import (archival)
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31
31
32 class RepoFactory(object):
32 class RepoFactory(object):
33 """
33 """
34 Utility to create instances of repository
34 Utility to create instances of repository
35
35
36 It provides internal caching of the `repo` object based on
36 It provides internal caching of the `repo` object based on
37 the :term:`call context`.
37 the :term:`call context`.
38 """
38 """
39 repo_type = None
39 repo_type = None
40
40
41 def __init__(self):
41 def __init__(self):
42 self._cache_region = region_meta.dogpile_cache_regions['repo_object']
42 self._cache_region = region_meta.dogpile_cache_regions['repo_object']
43
43
44 def _create_config(self, path, config):
44 def _create_config(self, path, config):
45 config = {}
45 config = {}
46 return config
46 return config
47
47
48 def _create_repo(self, wire, create):
48 def _create_repo(self, wire, create):
49 raise NotImplementedError()
49 raise NotImplementedError()
50
50
51 def repo(self, wire, create=False):
51 def repo(self, wire, create=False):
52 raise NotImplementedError()
52 raise NotImplementedError()
53
53
54
54
55 def obfuscate_qs(query_string):
55 def obfuscate_qs(query_string):
56 if query_string is None:
56 if query_string is None:
57 return None
57 return None
58
58
59 parsed = []
59 parsed = []
60 for k, v in urllib.parse.parse_qsl(query_string, keep_blank_values=True):
60 for k, v in urllib.parse.parse_qsl(query_string, keep_blank_values=True):
61 if k in ['auth_token', 'api_key']:
61 if k in ['auth_token', 'api_key']:
62 v = "*****"
62 v = "*****"
63 parsed.append((k, v))
63 parsed.append((k, v))
64
64
65 return '&'.join('{}{}'.format(
65 return '&'.join('{}{}'.format(
66 k, '={}'.format(v) if v else '') for k, v in parsed)
66 k, '={}'.format(v) if v else '') for k, v in parsed)
67
67
68
68
69 def raise_from_original(new_type):
69 def raise_from_original(new_type, org_exc: Exception):
70 """
70 """
71 Raise a new exception type with original args and traceback.
71 Raise a new exception type with original args and traceback.
72 """
72 """
73
73 exc_type, exc_value, exc_traceback = sys.exc_info()
74 exc_type, exc_value, exc_traceback = sys.exc_info()
74 new_exc = new_type(*exc_value.args)
75 new_exc = new_type(*exc_value.args)
76
75 # store the original traceback into the new exc
77 # store the original traceback into the new exc
76 new_exc._org_exc_tb = traceback.format_exc(exc_traceback)
78 new_exc._org_exc_tb = traceback.format_tb(exc_traceback)
77
79
78 try:
80 try:
79 raise new_exc.with_traceback(exc_traceback)
81 raise new_exc.with_traceback(exc_traceback)
80 finally:
82 finally:
81 del exc_traceback
83 del exc_traceback
82
84
83
85
84 class ArchiveNode(object):
86 class ArchiveNode(object):
85 def __init__(self, path, mode, is_link, raw_bytes):
87 def __init__(self, path, mode, is_link, raw_bytes):
86 self.path = path
88 self.path = path
87 self.mode = mode
89 self.mode = mode
88 self.is_link = is_link
90 self.is_link = is_link
89 self.raw_bytes = raw_bytes
91 self.raw_bytes = raw_bytes
90
92
91
93
92 def archive_repo(walker, archive_dest_path, kind, mtime, archive_at_path,
94 def archive_repo(walker, archive_dest_path, kind, mtime, archive_at_path,
93 archive_dir_name, commit_id, write_metadata=True, extra_metadata=None):
95 archive_dir_name, commit_id, write_metadata=True, extra_metadata=None):
94 """
96 """
95 walker should be a file walker, for example:
97 walker should be a file walker, for example:
96 def walker():
98 def walker():
97 for file_info in files:
99 for file_info in files:
98 yield ArchiveNode(fn, mode, is_link, ctx[fn].data)
100 yield ArchiveNode(fn, mode, is_link, ctx[fn].data)
99 """
101 """
100 extra_metadata = extra_metadata or {}
102 extra_metadata = extra_metadata or {}
101
103
102 if kind == "tgz":
104 if kind == "tgz":
103 archiver = archival.tarit(archive_dest_path, mtime, "gz")
105 archiver = archival.tarit(archive_dest_path, mtime, "gz")
104 elif kind == "tbz2":
106 elif kind == "tbz2":
105 archiver = archival.tarit(archive_dest_path, mtime, "bz2")
107 archiver = archival.tarit(archive_dest_path, mtime, "bz2")
106 elif kind == 'zip':
108 elif kind == 'zip':
107 archiver = archival.zipit(archive_dest_path, mtime)
109 archiver = archival.zipit(archive_dest_path, mtime)
108 else:
110 else:
109 raise exceptions.ArchiveException()(
111 raise exceptions.ArchiveException()(
110 'Remote does not support: "%s" archive type.' % kind)
112 'Remote does not support: "%s" archive type.' % kind)
111
113
112 for f in walker(commit_id, archive_at_path):
114 for f in walker(commit_id, archive_at_path):
113 f_path = os.path.join(archive_dir_name, f.path.lstrip('/'))
115 f_path = os.path.join(archive_dir_name, f.path.lstrip('/'))
114 try:
116 try:
115 archiver.addfile(f_path, f.mode, f.is_link, f.raw_bytes())
117 archiver.addfile(f_path, f.mode, f.is_link, f.raw_bytes())
116 except NoContentException:
118 except NoContentException:
117 # NOTE(marcink): this is a special case for SVN so we can create "empty"
119 # NOTE(marcink): this is a special case for SVN so we can create "empty"
118 # directories which arent supported by archiver
120 # directories which arent supported by archiver
119 archiver.addfile(os.path.join(f_path, '.dir'), f.mode, f.is_link, '')
121 archiver.addfile(os.path.join(f_path, '.dir'), f.mode, f.is_link, '')
120
122
121 if write_metadata:
123 if write_metadata:
122 metadata = dict([
124 metadata = dict([
123 ('commit_id', commit_id),
125 ('commit_id', commit_id),
124 ('mtime', mtime),
126 ('mtime', mtime),
125 ])
127 ])
126 metadata.update(extra_metadata)
128 metadata.update(extra_metadata)
127
129
128 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata.items()]
130 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata.items()]
129 f_path = os.path.join(archive_dir_name, '.archival.txt')
131 f_path = os.path.join(archive_dir_name, '.archival.txt')
130 archiver.addfile(f_path, 0o644, False, '\n'.join(meta))
132 archiver.addfile(f_path, 0o644, False, '\n'.join(meta))
131
133
132 return archiver.done()
134 return archiver.done()
@@ -1,207 +1,208 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import textwrap
22 import textwrap
23 import string
23 import string
24 import functools
24 import functools
25 import logging
25 import logging
26 import tempfile
26 import tempfile
27 import logging.config
27 import logging.config
28 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
29
29
30 # skip keys, that are set here, so we don't double process those
30 # skip keys, that are set here, so we don't double process those
31 set_keys = {
31 set_keys = {
32 '__file__': ''
32 '__file__': ''
33 }
33 }
34
34
35
35
36 def str2bool(_str):
36 def str2bool(_str):
37 """
37 """
38 returns True/False value from given string, it tries to translate the
38 returns True/False value from given string, it tries to translate the
39 string into boolean
39 string into boolean
40
40
41 :param _str: string value to translate into boolean
41 :param _str: string value to translate into boolean
42 :rtype: boolean
42 :rtype: boolean
43 :returns: boolean from given string
43 :returns: boolean from given string
44 """
44 """
45 if _str is None:
45 if _str is None:
46 return False
46 return False
47 if _str in (True, False):
47 if _str in (True, False):
48 return _str
48 return _str
49 _str = str(_str).strip().lower()
49 _str = str(_str).strip().lower()
50 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
50 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
51
51
52
52
53 def aslist(obj, sep=None, strip=True):
53 def aslist(obj, sep=None, strip=True):
54 """
54 """
55 Returns given string separated by sep as list
55 Returns given string separated by sep as list
56
56
57 :param obj:
57 :param obj:
58 :param sep:
58 :param sep:
59 :param strip:
59 :param strip:
60 """
60 """
61 if isinstance(obj, str):
61 if isinstance(obj, str):
62 if obj in ['', ""]:
62 if obj in ['', ""]:
63 return []
63 return []
64
64
65 lst = obj.split(sep)
65 lst = obj.split(sep)
66 if strip:
66 if strip:
67 lst = [v.strip() for v in lst]
67 lst = [v.strip() for v in lst]
68 return lst
68 return lst
69 elif isinstance(obj, (list, tuple)):
69 elif isinstance(obj, (list, tuple)):
70 return obj
70 return obj
71 elif obj is None:
71 elif obj is None:
72 return []
72 return []
73 else:
73 else:
74 return [obj]
74 return [obj]
75
75
76
76
77 class SettingsMaker(object):
77 class SettingsMaker(object):
78
78
79 def __init__(self, app_settings):
79 def __init__(self, app_settings):
80 self.settings = app_settings
80 self.settings = app_settings
81
81
82 @classmethod
82 @classmethod
83 def _bool_func(cls, input_val):
83 def _bool_func(cls, input_val):
84 if isinstance(input_val, unicode):
84 if isinstance(input_val, bytes):
85 input_val = input_val.encode('utf8')
85 # decode to str
86 input_val = input_val.decode('utf8')
86 return str2bool(input_val)
87 return str2bool(input_val)
87
88
88 @classmethod
89 @classmethod
89 def _int_func(cls, input_val):
90 def _int_func(cls, input_val):
90 return int(input_val)
91 return int(input_val)
91
92
92 @classmethod
93 @classmethod
93 def _list_func(cls, input_val, sep=','):
94 def _list_func(cls, input_val, sep=','):
94 return aslist(input_val, sep=sep)
95 return aslist(input_val, sep=sep)
95
96
96 @classmethod
97 @classmethod
97 def _string_func(cls, input_val, lower=True):
98 def _string_func(cls, input_val, lower=True):
98 if lower:
99 if lower:
99 input_val = input_val.lower()
100 input_val = input_val.lower()
100 return input_val
101 return input_val
101
102
102 @classmethod
103 @classmethod
103 def _float_func(cls, input_val):
104 def _float_func(cls, input_val):
104 return float(input_val)
105 return float(input_val)
105
106
106 @classmethod
107 @classmethod
107 def _dir_func(cls, input_val, ensure_dir=False, mode=0o755):
108 def _dir_func(cls, input_val, ensure_dir=False, mode=0o755):
108
109
109 # ensure we have our dir created
110 # ensure we have our dir created
110 if not os.path.isdir(input_val) and ensure_dir:
111 if not os.path.isdir(input_val) and ensure_dir:
111 os.makedirs(input_val, mode=mode)
112 os.makedirs(input_val, mode=mode)
112
113
113 if not os.path.isdir(input_val):
114 if not os.path.isdir(input_val):
114 raise Exception('Dir at {} does not exist'.format(input_val))
115 raise Exception('Dir at {} does not exist'.format(input_val))
115 return input_val
116 return input_val
116
117
117 @classmethod
118 @classmethod
118 def _file_path_func(cls, input_val, ensure_dir=False, mode=0o755):
119 def _file_path_func(cls, input_val, ensure_dir=False, mode=0o755):
119 dirname = os.path.dirname(input_val)
120 dirname = os.path.dirname(input_val)
120 cls._dir_func(dirname, ensure_dir=ensure_dir)
121 cls._dir_func(dirname, ensure_dir=ensure_dir)
121 return input_val
122 return input_val
122
123
123 @classmethod
124 @classmethod
124 def _key_transformator(cls, key):
125 def _key_transformator(cls, key):
125 return "{}_{}".format('RC'.upper(), key.upper().replace('.', '_').replace('-', '_'))
126 return "{}_{}".format('RC'.upper(), key.upper().replace('.', '_').replace('-', '_'))
126
127
127 def maybe_env_key(self, key):
128 def maybe_env_key(self, key):
128 # now maybe we have this KEY in env, search and use the value with higher priority.
129 # now maybe we have this KEY in env, search and use the value with higher priority.
129 transformed_key = self._key_transformator(key)
130 transformed_key = self._key_transformator(key)
130 envvar_value = os.environ.get(transformed_key)
131 envvar_value = os.environ.get(transformed_key)
131 if envvar_value:
132 if envvar_value:
132 log.debug('using `%s` key instead of `%s` key for config', transformed_key, key)
133 log.debug('using `%s` key instead of `%s` key for config', transformed_key, key)
133
134
134 return envvar_value
135 return envvar_value
135
136
136 def env_expand(self):
137 def env_expand(self):
137 replaced = {}
138 replaced = {}
138 for k, v in self.settings.items():
139 for k, v in self.settings.items():
139 if k not in set_keys:
140 if k not in set_keys:
140 envvar_value = self.maybe_env_key(k)
141 envvar_value = self.maybe_env_key(k)
141 if envvar_value:
142 if envvar_value:
142 replaced[k] = envvar_value
143 replaced[k] = envvar_value
143 set_keys[k] = envvar_value
144 set_keys[k] = envvar_value
144
145
145 # replace ALL keys updated
146 # replace ALL keys updated
146 self.settings.update(replaced)
147 self.settings.update(replaced)
147
148
148 def enable_logging(self, logging_conf=None, level='INFO', formatter='generic'):
149 def enable_logging(self, logging_conf=None, level='INFO', formatter='generic'):
149 """
150 """
150 Helper to enable debug on running instance
151 Helper to enable debug on running instance
151 :return:
152 :return:
152 """
153 """
153
154
154 if not str2bool(self.settings.get('logging.autoconfigure')):
155 if not str2bool(self.settings.get('logging.autoconfigure')):
155 log.info('logging configuration based on main .ini file')
156 log.info('logging configuration based on main .ini file')
156 return
157 return
157
158
158 if logging_conf is None:
159 if logging_conf is None:
159 logging_conf = self.settings.get('logging.logging_conf_file') or ''
160 logging_conf = self.settings.get('logging.logging_conf_file') or ''
160
161
161 if not os.path.isfile(logging_conf):
162 if not os.path.isfile(logging_conf):
162 log.error('Unable to setup logging based on %s, '
163 log.error('Unable to setup logging based on %s, '
163 'file does not exist.... specify path using logging.logging_conf_file= config setting. ', logging_conf)
164 'file does not exist.... specify path using logging.logging_conf_file= config setting. ', logging_conf)
164 return
165 return
165
166
166 with open(logging_conf, 'rb') as f:
167 with open(logging_conf, 'rb') as f:
167 ini_template = textwrap.dedent(f.read())
168 ini_template = textwrap.dedent(f.read())
168 ini_template = string.Template(ini_template).safe_substitute(
169 ini_template = string.Template(ini_template).safe_substitute(
169 RC_LOGGING_LEVEL=os.environ.get('RC_LOGGING_LEVEL', '') or level,
170 RC_LOGGING_LEVEL=os.environ.get('RC_LOGGING_LEVEL', '') or level,
170 RC_LOGGING_FORMATTER=os.environ.get('RC_LOGGING_FORMATTER', '') or formatter
171 RC_LOGGING_FORMATTER=os.environ.get('RC_LOGGING_FORMATTER', '') or formatter
171 )
172 )
172
173
173 with tempfile.NamedTemporaryFile(prefix='rc_logging_', suffix='.ini', delete=False) as f:
174 with tempfile.NamedTemporaryFile(prefix='rc_logging_', suffix='.ini', delete=False) as f:
174 log.info('Saved Temporary LOGGING config at %s', f.name)
175 log.info('Saved Temporary LOGGING config at %s', f.name)
175 f.write(ini_template)
176 f.write(ini_template)
176
177
177 logging.config.fileConfig(f.name)
178 logging.config.fileConfig(f.name)
178 os.remove(f.name)
179 os.remove(f.name)
179
180
180 def make_setting(self, key, default, lower=False, default_when_empty=False, parser=None):
181 def make_setting(self, key, default, lower=False, default_when_empty=False, parser=None):
181 input_val = self.settings.get(key, default)
182 input_val = self.settings.get(key, default)
182
183
183 if default_when_empty and not input_val:
184 if default_when_empty and not input_val:
184 # use default value when value is set in the config but it is empty
185 # use default value when value is set in the config but it is empty
185 input_val = default
186 input_val = default
186
187
187 parser_func = {
188 parser_func = {
188 'bool': self._bool_func,
189 'bool': self._bool_func,
189 'int': self._int_func,
190 'int': self._int_func,
190 'list': self._list_func,
191 'list': self._list_func,
191 'list:newline': functools.partial(self._list_func, sep='/n'),
192 'list:newline': functools.partial(self._list_func, sep='/n'),
192 'list:spacesep': functools.partial(self._list_func, sep=' '),
193 'list:spacesep': functools.partial(self._list_func, sep=' '),
193 'string': functools.partial(self._string_func, lower=lower),
194 'string': functools.partial(self._string_func, lower=lower),
194 'dir': self._dir_func,
195 'dir': self._dir_func,
195 'dir:ensured': functools.partial(self._dir_func, ensure_dir=True),
196 'dir:ensured': functools.partial(self._dir_func, ensure_dir=True),
196 'file': self._file_path_func,
197 'file': self._file_path_func,
197 'file:ensured': functools.partial(self._file_path_func, ensure_dir=True),
198 'file:ensured': functools.partial(self._file_path_func, ensure_dir=True),
198 None: lambda i: i
199 None: lambda i: i
199 }[parser]
200 }[parser]
200
201
201 envvar_value = self.maybe_env_key(key)
202 envvar_value = self.maybe_env_key(key)
202 if envvar_value:
203 if envvar_value:
203 input_val = envvar_value
204 input_val = envvar_value
204 set_keys[key] = input_val
205 set_keys[key] = input_val
205
206
206 self.settings[key] = parser_func(input_val)
207 self.settings[key] = parser_func(input_val)
207 return self.settings[key]
208 return self.settings[key]
@@ -1,54 +1,54 b''
1 """
1 """
2 Implementation of :class:`EchoApp`.
2 Implementation of :class:`EchoApp`.
3
3
4 This WSGI application will just echo back the data which it recieves.
4 This WSGI application will just echo back the data which it recieves.
5 """
5 """
6
6
7 import logging
7 import logging
8
8
9
9
10 log = logging.getLogger(__name__)
10 log = logging.getLogger(__name__)
11
11
12
12
13 class EchoApp(object):
13 class EchoApp(object):
14
14
15 def __init__(self, repo_path, repo_name, config):
15 def __init__(self, repo_path, repo_name, config):
16 self._repo_path = repo_path
16 self._repo_path = repo_path
17 log.info("EchoApp initialized for %s", repo_path)
17 log.info("EchoApp initialized for %s", repo_path)
18
18
19 def __call__(self, environ, start_response):
19 def __call__(self, environ, start_response):
20 log.debug("EchoApp called for %s", self._repo_path)
20 log.debug("EchoApp called for %s", self._repo_path)
21 log.debug("Content-Length: %s", environ.get('CONTENT_LENGTH'))
21 log.debug("Content-Length: %s", environ.get('CONTENT_LENGTH'))
22 environ['wsgi.input'].read()
22 environ['wsgi.input'].read()
23 status = '200 OK'
23 status = '200 OK'
24 headers = [('Content-Type', 'text/plain')]
24 headers = [('Content-Type', 'text/plain')]
25 start_response(status, headers)
25 start_response(status, headers)
26 return ["ECHO"]
26 return [b"ECHO"]
27
27
28
28
29 class EchoAppStream(object):
29 class EchoAppStream(object):
30
30
31 def __init__(self, repo_path, repo_name, config):
31 def __init__(self, repo_path, repo_name, config):
32 self._repo_path = repo_path
32 self._repo_path = repo_path
33 log.info("EchoApp initialized for %s", repo_path)
33 log.info("EchoApp initialized for %s", repo_path)
34
34
35 def __call__(self, environ, start_response):
35 def __call__(self, environ, start_response):
36 log.debug("EchoApp called for %s", self._repo_path)
36 log.debug("EchoApp called for %s", self._repo_path)
37 log.debug("Content-Length: %s", environ.get('CONTENT_LENGTH'))
37 log.debug("Content-Length: %s", environ.get('CONTENT_LENGTH'))
38 environ['wsgi.input'].read()
38 environ['wsgi.input'].read()
39 status = '200 OK'
39 status = '200 OK'
40 headers = [('Content-Type', 'text/plain')]
40 headers = [('Content-Type', 'text/plain')]
41 start_response(status, headers)
41 start_response(status, headers)
42
42
43 def generator():
43 def generator():
44 for _ in range(1000000):
44 for _ in range(1000000):
45 yield "ECHO"
45 yield b"ECHO_STREAM"
46 return generator()
46 return generator()
47
47
48
48
49 def create_app():
49 def create_app():
50 """
50 """
51 Allows to run this app directly in a WSGI server.
51 Allows to run this app directly in a WSGI server.
52 """
52 """
53 stub_config = {}
53 stub_config = {}
54 return EchoApp('stub_path', 'stub_name', stub_config)
54 return EchoApp('stub_path', 'stub_name', stub_config)
@@ -1,45 +1,45 b''
1 """
1 """
2 Provides the same API as :mod:`remote_wsgi`.
2 Provides the same API as :mod:`remote_wsgi`.
3
3
4 Uses the `EchoApp` instead of real implementations.
4 Uses the `EchoApp` instead of real implementations.
5 """
5 """
6
6
7 import logging
7 import logging
8
8
9 from .echo_app import EchoApp
9 from .echo_app import EchoApp
10 from vcsserver import wsgi_app_caller
10 from vcsserver import wsgi_app_caller
11
11
12
12
13 log = logging.getLogger(__name__)
13 log = logging.getLogger(__name__)
14
14
15
15
16 class GitRemoteWsgi(object):
16 class GitRemoteWsgi(object):
17 def handle(self, environ, input_data, *args, **kwargs):
17 def handle(self, environ, input_data, *args, **kwargs):
18 app = wsgi_app_caller.WSGIAppCaller(
18 app = wsgi_app_caller.WSGIAppCaller(
19 create_echo_wsgi_app(*args, **kwargs))
19 create_echo_wsgi_app(*args, **kwargs))
20
20
21 return app.handle(environ, input_data)
21 return app.handle(environ, input_data)
22
22
23
23
24 class HgRemoteWsgi(object):
24 class HgRemoteWsgi(object):
25 def handle(self, environ, input_data, *args, **kwargs):
25 def handle(self, environ, input_data, *args, **kwargs):
26 app = wsgi_app_caller.WSGIAppCaller(
26 app = wsgi_app_caller.WSGIAppCaller(
27 create_echo_wsgi_app(*args, **kwargs))
27 create_echo_wsgi_app(*args, **kwargs))
28
28
29 return app.handle(environ, input_data)
29 return app.handle(environ, input_data)
30
30
31
31
32 def create_echo_wsgi_app(repo_path, repo_name, config):
32 def create_echo_wsgi_app(repo_path, repo_name, config):
33 log.debug("Creating EchoApp WSGI application")
33 log.debug("Creating EchoApp WSGI application")
34
34
35 _assert_valid_config(config)
35 _assert_valid_config(config)
36
36
37 # Remaining items are forwarded to have the extras available
37 # Remaining items are forwarded to have the extras available
38 return EchoApp(repo_path, repo_name, config=config)
38 return EchoApp(repo_path, repo_name, config=config)
39
39
40
40
41 def _assert_valid_config(config):
41 def _assert_valid_config(config):
42 config = config.copy()
42 config = config.copy()
43
43
44 # This is what git needs from config at this stage
44 # This is what git needs from config at this stage
45 config.pop('git_update_server_info')
45 config.pop(b'git_update_server_info')
@@ -1,292 +1,292 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import re
18 import re
19 import logging
19 import logging
20 from wsgiref.util import FileWrapper
20 from wsgiref.util import FileWrapper
21
21
22 import simplejson as json
23 from pyramid.config import Configurator
22 from pyramid.config import Configurator
24 from pyramid.response import Response, FileIter
23 from pyramid.response import Response, FileIter
25 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
26 HTTPBadRequest, HTTPNotImplemented, HTTPNotFound, HTTPForbidden,
25 HTTPBadRequest, HTTPNotImplemented, HTTPNotFound, HTTPForbidden,
27 HTTPUnprocessableEntity)
26 HTTPUnprocessableEntity)
28
27
28 from vcsserver.lib.rc_json import json
29 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
29 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
30 from vcsserver.git_lfs.utils import safe_result, get_cython_compat_decorator
30 from vcsserver.git_lfs.utils import safe_result, get_cython_compat_decorator
31 from vcsserver.utils import safe_int
31 from vcsserver.utils import safe_int
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 GIT_LFS_CONTENT_TYPE = 'application/vnd.git-lfs' #+json ?
36 GIT_LFS_CONTENT_TYPE = 'application/vnd.git-lfs' #+json ?
37 GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))')
37 GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))')
38
38
39
39
40 def write_response_error(http_exception, text=None):
40 def write_response_error(http_exception, text=None):
41 content_type = GIT_LFS_CONTENT_TYPE + '+json'
41 content_type = GIT_LFS_CONTENT_TYPE + '+json'
42 _exception = http_exception(content_type=content_type)
42 _exception = http_exception(content_type=content_type)
43 _exception.content_type = content_type
43 _exception.content_type = content_type
44 if text:
44 if text:
45 _exception.body = json.dumps({'message': text})
45 _exception.text = json.dumps({'message': text})
46 log.debug('LFS: writing response of type %s to client with text:%s',
46 log.debug('LFS: writing response of type %s to client with text:%s',
47 http_exception, text)
47 http_exception, text)
48 return _exception
48 return _exception
49
49
50
50
51 class AuthHeaderRequired(object):
51 class AuthHeaderRequired(object):
52 """
52 """
53 Decorator to check if request has proper auth-header
53 Decorator to check if request has proper auth-header
54 """
54 """
55
55
56 def __call__(self, func):
56 def __call__(self, func):
57 return get_cython_compat_decorator(self.__wrapper, func)
57 return get_cython_compat_decorator(self.__wrapper, func)
58
58
59 def __wrapper(self, func, *fargs, **fkwargs):
59 def __wrapper(self, func, *fargs, **fkwargs):
60 request = fargs[1]
60 request = fargs[1]
61 auth = request.authorization
61 auth = request.authorization
62 if not auth:
62 if not auth:
63 return write_response_error(HTTPForbidden)
63 return write_response_error(HTTPForbidden)
64 return func(*fargs[1:], **fkwargs)
64 return func(*fargs[1:], **fkwargs)
65
65
66
66
67 # views
67 # views
68
68
69 def lfs_objects(request):
69 def lfs_objects(request):
70 # indicate not supported, V1 API
70 # indicate not supported, V1 API
71 log.warning('LFS: v1 api not supported, reporting it back to client')
71 log.warning('LFS: v1 api not supported, reporting it back to client')
72 return write_response_error(HTTPNotImplemented, 'LFS: v1 api not supported')
72 return write_response_error(HTTPNotImplemented, 'LFS: v1 api not supported')
73
73
74
74
75 @AuthHeaderRequired()
75 @AuthHeaderRequired()
76 def lfs_objects_batch(request):
76 def lfs_objects_batch(request):
77 """
77 """
78 The client sends the following information to the Batch endpoint to transfer some objects:
78 The client sends the following information to the Batch endpoint to transfer some objects:
79
79
80 operation - Should be download or upload.
80 operation - Should be download or upload.
81 transfers - An optional Array of String identifiers for transfer
81 transfers - An optional Array of String identifiers for transfer
82 adapters that the client has configured. If omitted, the basic
82 adapters that the client has configured. If omitted, the basic
83 transfer adapter MUST be assumed by the server.
83 transfer adapter MUST be assumed by the server.
84 objects - An Array of objects to download.
84 objects - An Array of objects to download.
85 oid - String OID of the LFS object.
85 oid - String OID of the LFS object.
86 size - Integer byte size of the LFS object. Must be at least zero.
86 size - Integer byte size of the LFS object. Must be at least zero.
87 """
87 """
88 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
88 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
89 auth = request.authorization
89 auth = request.authorization
90 repo = request.matchdict.get('repo')
90 repo = request.matchdict.get('repo')
91 data = request.json
91 data = request.json
92 operation = data.get('operation')
92 operation = data.get('operation')
93 http_scheme = request.registry.git_lfs_http_scheme
93 http_scheme = request.registry.git_lfs_http_scheme
94
94
95 if operation not in ('download', 'upload'):
95 if operation not in ('download', 'upload'):
96 log.debug('LFS: unsupported operation:%s', operation)
96 log.debug('LFS: unsupported operation:%s', operation)
97 return write_response_error(
97 return write_response_error(
98 HTTPBadRequest, 'unsupported operation mode: `%s`' % operation)
98 HTTPBadRequest, 'unsupported operation mode: `%s`' % operation)
99
99
100 if 'objects' not in data:
100 if 'objects' not in data:
101 log.debug('LFS: missing objects data')
101 log.debug('LFS: missing objects data')
102 return write_response_error(
102 return write_response_error(
103 HTTPBadRequest, 'missing objects data')
103 HTTPBadRequest, 'missing objects data')
104
104
105 log.debug('LFS: handling operation of type: %s', operation)
105 log.debug('LFS: handling operation of type: %s', operation)
106
106
107 objects = []
107 objects = []
108 for o in data['objects']:
108 for o in data['objects']:
109 try:
109 try:
110 oid = o['oid']
110 oid = o['oid']
111 obj_size = o['size']
111 obj_size = o['size']
112 except KeyError:
112 except KeyError:
113 log.exception('LFS, failed to extract data')
113 log.exception('LFS, failed to extract data')
114 return write_response_error(
114 return write_response_error(
115 HTTPBadRequest, 'unsupported data in objects')
115 HTTPBadRequest, 'unsupported data in objects')
116
116
117 obj_data = {'oid': oid}
117 obj_data = {'oid': oid}
118
118
119 obj_href = request.route_url('lfs_objects_oid', repo=repo, oid=oid,
119 obj_href = request.route_url('lfs_objects_oid', repo=repo, oid=oid,
120 _scheme=http_scheme)
120 _scheme=http_scheme)
121 obj_verify_href = request.route_url('lfs_objects_verify', repo=repo,
121 obj_verify_href = request.route_url('lfs_objects_verify', repo=repo,
122 _scheme=http_scheme)
122 _scheme=http_scheme)
123 store = LFSOidStore(
123 store = LFSOidStore(
124 oid, repo, store_location=request.registry.git_lfs_store_path)
124 oid, repo, store_location=request.registry.git_lfs_store_path)
125 handler = OidHandler(
125 handler = OidHandler(
126 store, repo, auth, oid, obj_size, obj_data,
126 store, repo, auth, oid, obj_size, obj_data,
127 obj_href, obj_verify_href)
127 obj_href, obj_verify_href)
128
128
129 # this verifies also OIDs
129 # this verifies also OIDs
130 actions, errors = handler.exec_operation(operation)
130 actions, errors = handler.exec_operation(operation)
131 if errors:
131 if errors:
132 log.warning('LFS: got following errors: %s', errors)
132 log.warning('LFS: got following errors: %s', errors)
133 obj_data['errors'] = errors
133 obj_data['errors'] = errors
134
134
135 if actions:
135 if actions:
136 obj_data['actions'] = actions
136 obj_data['actions'] = actions
137
137
138 obj_data['size'] = obj_size
138 obj_data['size'] = obj_size
139 obj_data['authenticated'] = True
139 obj_data['authenticated'] = True
140 objects.append(obj_data)
140 objects.append(obj_data)
141
141
142 result = {'objects': objects, 'transfer': 'basic'}
142 result = {'objects': objects, 'transfer': 'basic'}
143 log.debug('LFS Response %s', safe_result(result))
143 log.debug('LFS Response %s', safe_result(result))
144
144
145 return result
145 return result
146
146
147
147
148 def lfs_objects_oid_upload(request):
148 def lfs_objects_oid_upload(request):
149 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
149 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
150 repo = request.matchdict.get('repo')
150 repo = request.matchdict.get('repo')
151 oid = request.matchdict.get('oid')
151 oid = request.matchdict.get('oid')
152 store = LFSOidStore(
152 store = LFSOidStore(
153 oid, repo, store_location=request.registry.git_lfs_store_path)
153 oid, repo, store_location=request.registry.git_lfs_store_path)
154 engine = store.get_engine(mode='wb')
154 engine = store.get_engine(mode='wb')
155 log.debug('LFS: starting chunked write of LFS oid: %s to storage', oid)
155 log.debug('LFS: starting chunked write of LFS oid: %s to storage', oid)
156
156
157 body = request.environ['wsgi.input']
157 body = request.environ['wsgi.input']
158
158
159 with engine as f:
159 with engine as f:
160 blksize = 64 * 1024 # 64kb
160 blksize = 64 * 1024 # 64kb
161 while True:
161 while True:
162 # read in chunks as stream comes in from Gunicorn
162 # read in chunks as stream comes in from Gunicorn
163 # this is a specific Gunicorn support function.
163 # this is a specific Gunicorn support function.
164 # might work differently on waitress
164 # might work differently on waitress
165 chunk = body.read(blksize)
165 chunk = body.read(blksize)
166 if not chunk:
166 if not chunk:
167 break
167 break
168 f.write(chunk)
168 f.write(chunk)
169
169
170 return {'upload': 'ok'}
170 return {'upload': 'ok'}
171
171
172
172
173 def lfs_objects_oid_download(request):
173 def lfs_objects_oid_download(request):
174 repo = request.matchdict.get('repo')
174 repo = request.matchdict.get('repo')
175 oid = request.matchdict.get('oid')
175 oid = request.matchdict.get('oid')
176
176
177 store = LFSOidStore(
177 store = LFSOidStore(
178 oid, repo, store_location=request.registry.git_lfs_store_path)
178 oid, repo, store_location=request.registry.git_lfs_store_path)
179 if not store.has_oid():
179 if not store.has_oid():
180 log.debug('LFS: oid %s does not exists in store', oid)
180 log.debug('LFS: oid %s does not exists in store', oid)
181 return write_response_error(
181 return write_response_error(
182 HTTPNotFound, 'requested file with oid `%s` not found in store' % oid)
182 HTTPNotFound, 'requested file with oid `%s` not found in store' % oid)
183
183
184 # TODO(marcink): support range header ?
184 # TODO(marcink): support range header ?
185 # Range: bytes=0-, `bytes=(\d+)\-.*`
185 # Range: bytes=0-, `bytes=(\d+)\-.*`
186
186
187 f = open(store.oid_path, 'rb')
187 f = open(store.oid_path, 'rb')
188 response = Response(
188 response = Response(
189 content_type='application/octet-stream', app_iter=FileIter(f))
189 content_type='application/octet-stream', app_iter=FileIter(f))
190 response.headers.add('X-RC-LFS-Response-Oid', str(oid))
190 response.headers.add('X-RC-LFS-Response-Oid', str(oid))
191 return response
191 return response
192
192
193
193
194 def lfs_objects_verify(request):
194 def lfs_objects_verify(request):
195 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
195 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
196 repo = request.matchdict.get('repo')
196 repo = request.matchdict.get('repo')
197
197
198 data = request.json
198 data = request.json
199 oid = data.get('oid')
199 oid = data.get('oid')
200 size = safe_int(data.get('size'))
200 size = safe_int(data.get('size'))
201
201
202 if not (oid and size):
202 if not (oid and size):
203 return write_response_error(
203 return write_response_error(
204 HTTPBadRequest, 'missing oid and size in request data')
204 HTTPBadRequest, 'missing oid and size in request data')
205
205
206 store = LFSOidStore(
206 store = LFSOidStore(
207 oid, repo, store_location=request.registry.git_lfs_store_path)
207 oid, repo, store_location=request.registry.git_lfs_store_path)
208 if not store.has_oid():
208 if not store.has_oid():
209 log.debug('LFS: oid %s does not exists in store', oid)
209 log.debug('LFS: oid %s does not exists in store', oid)
210 return write_response_error(
210 return write_response_error(
211 HTTPNotFound, 'oid `%s` does not exists in store' % oid)
211 HTTPNotFound, 'oid `%s` does not exists in store' % oid)
212
212
213 store_size = store.size_oid()
213 store_size = store.size_oid()
214 if store_size != size:
214 if store_size != size:
215 msg = 'requested file size mismatch store size:%s requested:%s' % (
215 msg = 'requested file size mismatch store size:%s requested:%s' % (
216 store_size, size)
216 store_size, size)
217 return write_response_error(
217 return write_response_error(
218 HTTPUnprocessableEntity, msg)
218 HTTPUnprocessableEntity, msg)
219
219
220 return {'message': {'size': 'ok', 'in_store': 'ok'}}
220 return {'message': {'size': 'ok', 'in_store': 'ok'}}
221
221
222
222
223 def lfs_objects_lock(request):
223 def lfs_objects_lock(request):
224 return write_response_error(
224 return write_response_error(
225 HTTPNotImplemented, 'GIT LFS locking api not supported')
225 HTTPNotImplemented, 'GIT LFS locking api not supported')
226
226
227
227
228 def not_found(request):
228 def not_found(request):
229 return write_response_error(
229 return write_response_error(
230 HTTPNotFound, 'request path not found')
230 HTTPNotFound, 'request path not found')
231
231
232
232
233 def lfs_disabled(request):
233 def lfs_disabled(request):
234 return write_response_error(
234 return write_response_error(
235 HTTPNotImplemented, 'GIT LFS disabled for this repo')
235 HTTPNotImplemented, 'GIT LFS disabled for this repo')
236
236
237
237
238 def git_lfs_app(config):
238 def git_lfs_app(config):
239
239
240 # v1 API deprecation endpoint
240 # v1 API deprecation endpoint
241 config.add_route('lfs_objects',
241 config.add_route('lfs_objects',
242 '/{repo:.*?[^/]}/info/lfs/objects')
242 '/{repo:.*?[^/]}/info/lfs/objects')
243 config.add_view(lfs_objects, route_name='lfs_objects',
243 config.add_view(lfs_objects, route_name='lfs_objects',
244 request_method='POST', renderer='json')
244 request_method='POST', renderer='json')
245
245
246 # locking API
246 # locking API
247 config.add_route('lfs_objects_lock',
247 config.add_route('lfs_objects_lock',
248 '/{repo:.*?[^/]}/info/lfs/locks')
248 '/{repo:.*?[^/]}/info/lfs/locks')
249 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock',
249 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock',
250 request_method=('POST', 'GET'), renderer='json')
250 request_method=('POST', 'GET'), renderer='json')
251
251
252 config.add_route('lfs_objects_lock_verify',
252 config.add_route('lfs_objects_lock_verify',
253 '/{repo:.*?[^/]}/info/lfs/locks/verify')
253 '/{repo:.*?[^/]}/info/lfs/locks/verify')
254 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock_verify',
254 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock_verify',
255 request_method=('POST', 'GET'), renderer='json')
255 request_method=('POST', 'GET'), renderer='json')
256
256
257 # batch API
257 # batch API
258 config.add_route('lfs_objects_batch',
258 config.add_route('lfs_objects_batch',
259 '/{repo:.*?[^/]}/info/lfs/objects/batch')
259 '/{repo:.*?[^/]}/info/lfs/objects/batch')
260 config.add_view(lfs_objects_batch, route_name='lfs_objects_batch',
260 config.add_view(lfs_objects_batch, route_name='lfs_objects_batch',
261 request_method='POST', renderer='json')
261 request_method='POST', renderer='json')
262
262
263 # oid upload/download API
263 # oid upload/download API
264 config.add_route('lfs_objects_oid',
264 config.add_route('lfs_objects_oid',
265 '/{repo:.*?[^/]}/info/lfs/objects/{oid}')
265 '/{repo:.*?[^/]}/info/lfs/objects/{oid}')
266 config.add_view(lfs_objects_oid_upload, route_name='lfs_objects_oid',
266 config.add_view(lfs_objects_oid_upload, route_name='lfs_objects_oid',
267 request_method='PUT', renderer='json')
267 request_method='PUT', renderer='json')
268 config.add_view(lfs_objects_oid_download, route_name='lfs_objects_oid',
268 config.add_view(lfs_objects_oid_download, route_name='lfs_objects_oid',
269 request_method='GET', renderer='json')
269 request_method='GET', renderer='json')
270
270
271 # verification API
271 # verification API
272 config.add_route('lfs_objects_verify',
272 config.add_route('lfs_objects_verify',
273 '/{repo:.*?[^/]}/info/lfs/verify')
273 '/{repo:.*?[^/]}/info/lfs/verify')
274 config.add_view(lfs_objects_verify, route_name='lfs_objects_verify',
274 config.add_view(lfs_objects_verify, route_name='lfs_objects_verify',
275 request_method='POST', renderer='json')
275 request_method='POST', renderer='json')
276
276
277 # not found handler for API
277 # not found handler for API
278 config.add_notfound_view(not_found, renderer='json')
278 config.add_notfound_view(not_found, renderer='json')
279
279
280
280
281 def create_app(git_lfs_enabled, git_lfs_store_path, git_lfs_http_scheme):
281 def create_app(git_lfs_enabled, git_lfs_store_path, git_lfs_http_scheme):
282 config = Configurator()
282 config = Configurator()
283 if git_lfs_enabled:
283 if git_lfs_enabled:
284 config.include(git_lfs_app)
284 config.include(git_lfs_app)
285 config.registry.git_lfs_store_path = git_lfs_store_path
285 config.registry.git_lfs_store_path = git_lfs_store_path
286 config.registry.git_lfs_http_scheme = git_lfs_http_scheme
286 config.registry.git_lfs_http_scheme = git_lfs_http_scheme
287 else:
287 else:
288 # not found handler for API, reporting disabled LFS support
288 # not found handler for API, reporting disabled LFS support
289 config.add_notfound_view(lfs_disabled, renderer='json')
289 config.add_notfound_view(lfs_disabled, renderer='json')
290
290
291 app = config.make_wsgi_app()
291 app = config.make_wsgi_app()
292 return app
292 return app
@@ -1,272 +1,273 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import pytest
19 import pytest
20 from webtest.app import TestApp as WebObTestApp
20 from webtest.app import TestApp as WebObTestApp
21 import simplejson as json
22
21
22 from vcsserver.lib.rc_json import json
23 from vcsserver.utils import safe_bytes
23 from vcsserver.git_lfs.app import create_app
24 from vcsserver.git_lfs.app import create_app
24
25
25
26
26 @pytest.fixture(scope='function')
27 @pytest.fixture(scope='function')
27 def git_lfs_app(tmpdir):
28 def git_lfs_app(tmpdir):
28 custom_app = WebObTestApp(create_app(
29 custom_app = WebObTestApp(create_app(
29 git_lfs_enabled=True, git_lfs_store_path=str(tmpdir),
30 git_lfs_enabled=True, git_lfs_store_path=str(tmpdir),
30 git_lfs_http_scheme='http'))
31 git_lfs_http_scheme='http'))
31 custom_app._store = str(tmpdir)
32 custom_app._store = str(tmpdir)
32 return custom_app
33 return custom_app
33
34
34
35
35 @pytest.fixture(scope='function')
36 @pytest.fixture(scope='function')
36 def git_lfs_https_app(tmpdir):
37 def git_lfs_https_app(tmpdir):
37 custom_app = WebObTestApp(create_app(
38 custom_app = WebObTestApp(create_app(
38 git_lfs_enabled=True, git_lfs_store_path=str(tmpdir),
39 git_lfs_enabled=True, git_lfs_store_path=str(tmpdir),
39 git_lfs_http_scheme='https'))
40 git_lfs_http_scheme='https'))
40 custom_app._store = str(tmpdir)
41 custom_app._store = str(tmpdir)
41 return custom_app
42 return custom_app
42
43
43
44
44 @pytest.fixture()
45 @pytest.fixture()
45 def http_auth():
46 def http_auth():
46 return {'HTTP_AUTHORIZATION': "Basic XXXXX"}
47 return {'HTTP_AUTHORIZATION': "Basic XXXXX"}
47
48
48
49
49 class TestLFSApplication(object):
50 class TestLFSApplication(object):
50
51
51 def test_app_wrong_path(self, git_lfs_app):
52 def test_app_wrong_path(self, git_lfs_app):
52 git_lfs_app.get('/repo/info/lfs/xxx', status=404)
53 git_lfs_app.get('/repo/info/lfs/xxx', status=404)
53
54
54 def test_app_deprecated_endpoint(self, git_lfs_app):
55 def test_app_deprecated_endpoint(self, git_lfs_app):
55 response = git_lfs_app.post('/repo/info/lfs/objects', status=501)
56 response = git_lfs_app.post('/repo/info/lfs/objects', status=501)
56 assert response.status_code == 501
57 assert response.status_code == 501
57 assert json.loads(response.text) == {'message': 'LFS: v1 api not supported'}
58 assert json.loads(response.text) == {'message': 'LFS: v1 api not supported'}
58
59
59 def test_app_lock_verify_api_not_available(self, git_lfs_app):
60 def test_app_lock_verify_api_not_available(self, git_lfs_app):
60 response = git_lfs_app.post('/repo/info/lfs/locks/verify', status=501)
61 response = git_lfs_app.post('/repo/info/lfs/locks/verify', status=501)
61 assert response.status_code == 501
62 assert response.status_code == 501
62 assert json.loads(response.text) == {
63 assert json.loads(response.text) == {
63 'message': 'GIT LFS locking api not supported'}
64 'message': 'GIT LFS locking api not supported'}
64
65
65 def test_app_lock_api_not_available(self, git_lfs_app):
66 def test_app_lock_api_not_available(self, git_lfs_app):
66 response = git_lfs_app.post('/repo/info/lfs/locks', status=501)
67 response = git_lfs_app.post('/repo/info/lfs/locks', status=501)
67 assert response.status_code == 501
68 assert response.status_code == 501
68 assert json.loads(response.text) == {
69 assert json.loads(response.text) == {
69 'message': 'GIT LFS locking api not supported'}
70 'message': 'GIT LFS locking api not supported'}
70
71
71 def test_app_batch_api_missing_auth(self, git_lfs_app):
72 def test_app_batch_api_missing_auth(self, git_lfs_app):
72 git_lfs_app.post_json(
73 git_lfs_app.post_json(
73 '/repo/info/lfs/objects/batch', params={}, status=403)
74 '/repo/info/lfs/objects/batch', params={}, status=403)
74
75
75 def test_app_batch_api_unsupported_operation(self, git_lfs_app, http_auth):
76 def test_app_batch_api_unsupported_operation(self, git_lfs_app, http_auth):
76 response = git_lfs_app.post_json(
77 response = git_lfs_app.post_json(
77 '/repo/info/lfs/objects/batch', params={}, status=400,
78 '/repo/info/lfs/objects/batch', params={}, status=400,
78 extra_environ=http_auth)
79 extra_environ=http_auth)
79 assert json.loads(response.text) == {
80 assert json.loads(response.text) == {
80 'message': 'unsupported operation mode: `None`'}
81 'message': 'unsupported operation mode: `None`'}
81
82
82 def test_app_batch_api_missing_objects(self, git_lfs_app, http_auth):
83 def test_app_batch_api_missing_objects(self, git_lfs_app, http_auth):
83 response = git_lfs_app.post_json(
84 response = git_lfs_app.post_json(
84 '/repo/info/lfs/objects/batch', params={'operation': 'download'},
85 '/repo/info/lfs/objects/batch', params={'operation': 'download'},
85 status=400, extra_environ=http_auth)
86 status=400, extra_environ=http_auth)
86 assert json.loads(response.text) == {
87 assert json.loads(response.text) == {
87 'message': 'missing objects data'}
88 'message': 'missing objects data'}
88
89
89 def test_app_batch_api_unsupported_data_in_objects(
90 def test_app_batch_api_unsupported_data_in_objects(
90 self, git_lfs_app, http_auth):
91 self, git_lfs_app, http_auth):
91 params = {'operation': 'download',
92 params = {'operation': 'download',
92 'objects': [{}]}
93 'objects': [{}]}
93 response = git_lfs_app.post_json(
94 response = git_lfs_app.post_json(
94 '/repo/info/lfs/objects/batch', params=params, status=400,
95 '/repo/info/lfs/objects/batch', params=params, status=400,
95 extra_environ=http_auth)
96 extra_environ=http_auth)
96 assert json.loads(response.text) == {
97 assert json.loads(response.text) == {
97 'message': 'unsupported data in objects'}
98 'message': 'unsupported data in objects'}
98
99
99 def test_app_batch_api_download_missing_object(
100 def test_app_batch_api_download_missing_object(
100 self, git_lfs_app, http_auth):
101 self, git_lfs_app, http_auth):
101 params = {'operation': 'download',
102 params = {'operation': 'download',
102 'objects': [{'oid': '123', 'size': '1024'}]}
103 'objects': [{'oid': '123', 'size': '1024'}]}
103 response = git_lfs_app.post_json(
104 response = git_lfs_app.post_json(
104 '/repo/info/lfs/objects/batch', params=params,
105 '/repo/info/lfs/objects/batch', params=params,
105 extra_environ=http_auth)
106 extra_environ=http_auth)
106
107
107 expected_objects = [
108 expected_objects = [
108 {'authenticated': True,
109 {'authenticated': True,
109 'errors': {'error': {
110 'errors': {'error': {
110 'code': 404,
111 'code': 404,
111 'message': 'object: 123 does not exist in store'}},
112 'message': 'object: 123 does not exist in store'}},
112 'oid': '123',
113 'oid': '123',
113 'size': '1024'}
114 'size': '1024'}
114 ]
115 ]
115 assert json.loads(response.text) == {
116 assert json.loads(response.text) == {
116 'objects': expected_objects, 'transfer': 'basic'}
117 'objects': expected_objects, 'transfer': 'basic'}
117
118
118 def test_app_batch_api_download(self, git_lfs_app, http_auth):
119 def test_app_batch_api_download(self, git_lfs_app, http_auth):
119 oid = '456'
120 oid = '456'
120 oid_path = os.path.join(git_lfs_app._store, oid)
121 oid_path = os.path.join(git_lfs_app._store, oid)
121 if not os.path.isdir(os.path.dirname(oid_path)):
122 if not os.path.isdir(os.path.dirname(oid_path)):
122 os.makedirs(os.path.dirname(oid_path))
123 os.makedirs(os.path.dirname(oid_path))
123 with open(oid_path, 'wb') as f:
124 with open(oid_path, 'wb') as f:
124 f.write('OID_CONTENT')
125 f.write(safe_bytes('OID_CONTENT'))
125
126
126 params = {'operation': 'download',
127 params = {'operation': 'download',
127 'objects': [{'oid': oid, 'size': '1024'}]}
128 'objects': [{'oid': oid, 'size': '1024'}]}
128 response = git_lfs_app.post_json(
129 response = git_lfs_app.post_json(
129 '/repo/info/lfs/objects/batch', params=params,
130 '/repo/info/lfs/objects/batch', params=params,
130 extra_environ=http_auth)
131 extra_environ=http_auth)
131
132
132 expected_objects = [
133 expected_objects = [
133 {'authenticated': True,
134 {'authenticated': True,
134 'actions': {
135 'actions': {
135 'download': {
136 'download': {
136 'header': {'Authorization': 'Basic XXXXX'},
137 'header': {'Authorization': 'Basic XXXXX'},
137 'href': 'http://localhost/repo/info/lfs/objects/456'},
138 'href': 'http://localhost/repo/info/lfs/objects/456'},
138 },
139 },
139 'oid': '456',
140 'oid': '456',
140 'size': '1024'}
141 'size': '1024'}
141 ]
142 ]
142 assert json.loads(response.text) == {
143 assert json.loads(response.text) == {
143 'objects': expected_objects, 'transfer': 'basic'}
144 'objects': expected_objects, 'transfer': 'basic'}
144
145
145 def test_app_batch_api_upload(self, git_lfs_app, http_auth):
146 def test_app_batch_api_upload(self, git_lfs_app, http_auth):
146 params = {'operation': 'upload',
147 params = {'operation': 'upload',
147 'objects': [{'oid': '123', 'size': '1024'}]}
148 'objects': [{'oid': '123', 'size': '1024'}]}
148 response = git_lfs_app.post_json(
149 response = git_lfs_app.post_json(
149 '/repo/info/lfs/objects/batch', params=params,
150 '/repo/info/lfs/objects/batch', params=params,
150 extra_environ=http_auth)
151 extra_environ=http_auth)
151 expected_objects = [
152 expected_objects = [
152 {'authenticated': True,
153 {'authenticated': True,
153 'actions': {
154 'actions': {
154 'upload': {
155 'upload': {
155 'header': {'Authorization': 'Basic XXXXX',
156 'header': {'Authorization': 'Basic XXXXX',
156 'Transfer-Encoding': 'chunked'},
157 'Transfer-Encoding': 'chunked'},
157 'href': 'http://localhost/repo/info/lfs/objects/123'},
158 'href': 'http://localhost/repo/info/lfs/objects/123'},
158 'verify': {
159 'verify': {
159 'header': {'Authorization': 'Basic XXXXX'},
160 'header': {'Authorization': 'Basic XXXXX'},
160 'href': 'http://localhost/repo/info/lfs/verify'}
161 'href': 'http://localhost/repo/info/lfs/verify'}
161 },
162 },
162 'oid': '123',
163 'oid': '123',
163 'size': '1024'}
164 'size': '1024'}
164 ]
165 ]
165 assert json.loads(response.text) == {
166 assert json.loads(response.text) == {
166 'objects': expected_objects, 'transfer': 'basic'}
167 'objects': expected_objects, 'transfer': 'basic'}
167
168
168 def test_app_batch_api_upload_for_https(self, git_lfs_https_app, http_auth):
169 def test_app_batch_api_upload_for_https(self, git_lfs_https_app, http_auth):
169 params = {'operation': 'upload',
170 params = {'operation': 'upload',
170 'objects': [{'oid': '123', 'size': '1024'}]}
171 'objects': [{'oid': '123', 'size': '1024'}]}
171 response = git_lfs_https_app.post_json(
172 response = git_lfs_https_app.post_json(
172 '/repo/info/lfs/objects/batch', params=params,
173 '/repo/info/lfs/objects/batch', params=params,
173 extra_environ=http_auth)
174 extra_environ=http_auth)
174 expected_objects = [
175 expected_objects = [
175 {'authenticated': True,
176 {'authenticated': True,
176 'actions': {
177 'actions': {
177 'upload': {
178 'upload': {
178 'header': {'Authorization': 'Basic XXXXX',
179 'header': {'Authorization': 'Basic XXXXX',
179 'Transfer-Encoding': 'chunked'},
180 'Transfer-Encoding': 'chunked'},
180 'href': 'https://localhost/repo/info/lfs/objects/123'},
181 'href': 'https://localhost/repo/info/lfs/objects/123'},
181 'verify': {
182 'verify': {
182 'header': {'Authorization': 'Basic XXXXX'},
183 'header': {'Authorization': 'Basic XXXXX'},
183 'href': 'https://localhost/repo/info/lfs/verify'}
184 'href': 'https://localhost/repo/info/lfs/verify'}
184 },
185 },
185 'oid': '123',
186 'oid': '123',
186 'size': '1024'}
187 'size': '1024'}
187 ]
188 ]
188 assert json.loads(response.text) == {
189 assert json.loads(response.text) == {
189 'objects': expected_objects, 'transfer': 'basic'}
190 'objects': expected_objects, 'transfer': 'basic'}
190
191
191 def test_app_verify_api_missing_data(self, git_lfs_app):
192 def test_app_verify_api_missing_data(self, git_lfs_app):
192 params = {'oid': 'missing'}
193 params = {'oid': 'missing'}
193 response = git_lfs_app.post_json(
194 response = git_lfs_app.post_json(
194 '/repo/info/lfs/verify', params=params,
195 '/repo/info/lfs/verify', params=params,
195 status=400)
196 status=400)
196
197
197 assert json.loads(response.text) == {
198 assert json.loads(response.text) == {
198 'message': 'missing oid and size in request data'}
199 'message': 'missing oid and size in request data'}
199
200
200 def test_app_verify_api_missing_obj(self, git_lfs_app):
201 def test_app_verify_api_missing_obj(self, git_lfs_app):
201 params = {'oid': 'missing', 'size': '1024'}
202 params = {'oid': 'missing', 'size': '1024'}
202 response = git_lfs_app.post_json(
203 response = git_lfs_app.post_json(
203 '/repo/info/lfs/verify', params=params,
204 '/repo/info/lfs/verify', params=params,
204 status=404)
205 status=404)
205
206
206 assert json.loads(response.text) == {
207 assert json.loads(response.text) == {
207 'message': 'oid `missing` does not exists in store'}
208 'message': 'oid `missing` does not exists in store'}
208
209
209 def test_app_verify_api_size_mismatch(self, git_lfs_app):
210 def test_app_verify_api_size_mismatch(self, git_lfs_app):
210 oid = 'existing'
211 oid = 'existing'
211 oid_path = os.path.join(git_lfs_app._store, oid)
212 oid_path = os.path.join(git_lfs_app._store, oid)
212 if not os.path.isdir(os.path.dirname(oid_path)):
213 if not os.path.isdir(os.path.dirname(oid_path)):
213 os.makedirs(os.path.dirname(oid_path))
214 os.makedirs(os.path.dirname(oid_path))
214 with open(oid_path, 'wb') as f:
215 with open(oid_path, 'wb') as f:
215 f.write('OID_CONTENT')
216 f.write(safe_bytes('OID_CONTENT'))
216
217
217 params = {'oid': oid, 'size': '1024'}
218 params = {'oid': oid, 'size': '1024'}
218 response = git_lfs_app.post_json(
219 response = git_lfs_app.post_json(
219 '/repo/info/lfs/verify', params=params, status=422)
220 '/repo/info/lfs/verify', params=params, status=422)
220
221
221 assert json.loads(response.text) == {
222 assert json.loads(response.text) == {
222 'message': 'requested file size mismatch '
223 'message': 'requested file size mismatch '
223 'store size:11 requested:1024'}
224 'store size:11 requested:1024'}
224
225
225 def test_app_verify_api(self, git_lfs_app):
226 def test_app_verify_api(self, git_lfs_app):
226 oid = 'existing'
227 oid = 'existing'
227 oid_path = os.path.join(git_lfs_app._store, oid)
228 oid_path = os.path.join(git_lfs_app._store, oid)
228 if not os.path.isdir(os.path.dirname(oid_path)):
229 if not os.path.isdir(os.path.dirname(oid_path)):
229 os.makedirs(os.path.dirname(oid_path))
230 os.makedirs(os.path.dirname(oid_path))
230 with open(oid_path, 'wb') as f:
231 with open(oid_path, 'wb') as f:
231 f.write('OID_CONTENT')
232 f.write(safe_bytes('OID_CONTENT'))
232
233
233 params = {'oid': oid, 'size': 11}
234 params = {'oid': oid, 'size': 11}
234 response = git_lfs_app.post_json(
235 response = git_lfs_app.post_json(
235 '/repo/info/lfs/verify', params=params)
236 '/repo/info/lfs/verify', params=params)
236
237
237 assert json.loads(response.text) == {
238 assert json.loads(response.text) == {
238 'message': {'size': 'ok', 'in_store': 'ok'}}
239 'message': {'size': 'ok', 'in_store': 'ok'}}
239
240
240 def test_app_download_api_oid_not_existing(self, git_lfs_app):
241 def test_app_download_api_oid_not_existing(self, git_lfs_app):
241 oid = 'missing'
242 oid = 'missing'
242
243
243 response = git_lfs_app.get(
244 response = git_lfs_app.get(
244 '/repo/info/lfs/objects/{oid}'.format(oid=oid), status=404)
245 '/repo/info/lfs/objects/{oid}'.format(oid=oid), status=404)
245
246
246 assert json.loads(response.text) == {
247 assert json.loads(response.text) == {
247 'message': 'requested file with oid `missing` not found in store'}
248 'message': 'requested file with oid `missing` not found in store'}
248
249
249 def test_app_download_api(self, git_lfs_app):
250 def test_app_download_api(self, git_lfs_app):
250 oid = 'existing'
251 oid = 'existing'
251 oid_path = os.path.join(git_lfs_app._store, oid)
252 oid_path = os.path.join(git_lfs_app._store, oid)
252 if not os.path.isdir(os.path.dirname(oid_path)):
253 if not os.path.isdir(os.path.dirname(oid_path)):
253 os.makedirs(os.path.dirname(oid_path))
254 os.makedirs(os.path.dirname(oid_path))
254 with open(oid_path, 'wb') as f:
255 with open(oid_path, 'wb') as f:
255 f.write('OID_CONTENT')
256 f.write(safe_bytes('OID_CONTENT'))
256
257
257 response = git_lfs_app.get(
258 response = git_lfs_app.get(
258 '/repo/info/lfs/objects/{oid}'.format(oid=oid))
259 '/repo/info/lfs/objects/{oid}'.format(oid=oid))
259 assert response
260 assert response
260
261
261 def test_app_upload(self, git_lfs_app):
262 def test_app_upload(self, git_lfs_app):
262 oid = 'uploaded'
263 oid = 'uploaded'
263
264
264 response = git_lfs_app.put(
265 response = git_lfs_app.put(
265 '/repo/info/lfs/objects/{oid}'.format(oid=oid), params='CONTENT')
266 '/repo/info/lfs/objects/{oid}'.format(oid=oid), params='CONTENT')
266
267
267 assert json.loads(response.text) == {'upload': 'ok'}
268 assert json.loads(response.text) == {'upload': 'ok'}
268
269
269 # verify that we actually wrote that OID
270 # verify that we actually wrote that OID
270 oid_path = os.path.join(git_lfs_app._store, oid)
271 oid_path = os.path.join(git_lfs_app._store, oid)
271 assert os.path.isfile(oid_path)
272 assert os.path.isfile(oid_path)
272 assert 'CONTENT' == open(oid_path).read()
273 assert 'CONTENT' == open(oid_path).read()
@@ -1,141 +1,142 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import pytest
19 import pytest
20 from vcsserver.utils import safe_bytes
20 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
21 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
21
22
22
23
23 @pytest.fixture()
24 @pytest.fixture()
24 def lfs_store(tmpdir):
25 def lfs_store(tmpdir):
25 repo = 'test'
26 repo = 'test'
26 oid = '123456789'
27 oid = '123456789'
27 store = LFSOidStore(oid=oid, repo=repo, store_location=str(tmpdir))
28 store = LFSOidStore(oid=oid, repo=repo, store_location=str(tmpdir))
28 return store
29 return store
29
30
30
31
31 @pytest.fixture()
32 @pytest.fixture()
32 def oid_handler(lfs_store):
33 def oid_handler(lfs_store):
33 store = lfs_store
34 store = lfs_store
34 repo = store.repo
35 repo = store.repo
35 oid = store.oid
36 oid = store.oid
36
37
37 oid_handler = OidHandler(
38 oid_handler = OidHandler(
38 store=store, repo_name=repo, auth=('basic', 'xxxx'),
39 store=store, repo_name=repo, auth=('basic', 'xxxx'),
39 oid=oid,
40 oid=oid,
40 obj_size='1024', obj_data={}, obj_href='http://localhost/handle_oid',
41 obj_size='1024', obj_data={}, obj_href='http://localhost/handle_oid',
41 obj_verify_href='http://localhost/verify')
42 obj_verify_href='http://localhost/verify')
42 return oid_handler
43 return oid_handler
43
44
44
45
45 class TestOidHandler(object):
46 class TestOidHandler(object):
46
47
47 @pytest.mark.parametrize('exec_action', [
48 @pytest.mark.parametrize('exec_action', [
48 'download',
49 'download',
49 'upload',
50 'upload',
50 ])
51 ])
51 def test_exec_action(self, exec_action, oid_handler):
52 def test_exec_action(self, exec_action, oid_handler):
52 handler = oid_handler.exec_operation(exec_action)
53 handler = oid_handler.exec_operation(exec_action)
53 assert handler
54 assert handler
54
55
55 def test_exec_action_undefined(self, oid_handler):
56 def test_exec_action_undefined(self, oid_handler):
56 with pytest.raises(AttributeError):
57 with pytest.raises(AttributeError):
57 oid_handler.exec_operation('wrong')
58 oid_handler.exec_operation('wrong')
58
59
59 def test_download_oid_not_existing(self, oid_handler):
60 def test_download_oid_not_existing(self, oid_handler):
60 response, has_errors = oid_handler.exec_operation('download')
61 response, has_errors = oid_handler.exec_operation('download')
61
62
62 assert response is None
63 assert response is None
63 assert has_errors['error'] == {
64 assert has_errors['error'] == {
64 'code': 404,
65 'code': 404,
65 'message': 'object: 123456789 does not exist in store'}
66 'message': 'object: 123456789 does not exist in store'}
66
67
67 def test_download_oid(self, oid_handler):
68 def test_download_oid(self, oid_handler):
68 store = oid_handler.get_store()
69 store = oid_handler.get_store()
69 if not os.path.isdir(os.path.dirname(store.oid_path)):
70 if not os.path.isdir(os.path.dirname(store.oid_path)):
70 os.makedirs(os.path.dirname(store.oid_path))
71 os.makedirs(os.path.dirname(store.oid_path))
71
72
72 with open(store.oid_path, 'wb') as f:
73 with open(store.oid_path, 'wb') as f:
73 f.write('CONTENT')
74 f.write(safe_bytes('CONTENT'))
74
75
75 response, has_errors = oid_handler.exec_operation('download')
76 response, has_errors = oid_handler.exec_operation('download')
76
77
77 assert has_errors is None
78 assert has_errors is None
78 assert response['download'] == {
79 assert response['download'] == {
79 'header': {'Authorization': 'basic xxxx'},
80 'header': {'Authorization': 'basic xxxx'},
80 'href': 'http://localhost/handle_oid'
81 'href': 'http://localhost/handle_oid'
81 }
82 }
82
83
83 def test_upload_oid_that_exists(self, oid_handler):
84 def test_upload_oid_that_exists(self, oid_handler):
84 store = oid_handler.get_store()
85 store = oid_handler.get_store()
85 if not os.path.isdir(os.path.dirname(store.oid_path)):
86 if not os.path.isdir(os.path.dirname(store.oid_path)):
86 os.makedirs(os.path.dirname(store.oid_path))
87 os.makedirs(os.path.dirname(store.oid_path))
87
88
88 with open(store.oid_path, 'wb') as f:
89 with open(store.oid_path, 'wb') as f:
89 f.write('CONTENT')
90 f.write(safe_bytes('CONTENT'))
90 oid_handler.obj_size = 7
91 oid_handler.obj_size = 7
91 response, has_errors = oid_handler.exec_operation('upload')
92 response, has_errors = oid_handler.exec_operation('upload')
92 assert has_errors is None
93 assert has_errors is None
93 assert response is None
94 assert response is None
94
95
95 def test_upload_oid_that_exists_but_has_wrong_size(self, oid_handler):
96 def test_upload_oid_that_exists_but_has_wrong_size(self, oid_handler):
96 store = oid_handler.get_store()
97 store = oid_handler.get_store()
97 if not os.path.isdir(os.path.dirname(store.oid_path)):
98 if not os.path.isdir(os.path.dirname(store.oid_path)):
98 os.makedirs(os.path.dirname(store.oid_path))
99 os.makedirs(os.path.dirname(store.oid_path))
99
100
100 with open(store.oid_path, 'wb') as f:
101 with open(store.oid_path, 'wb') as f:
101 f.write('CONTENT')
102 f.write(safe_bytes('CONTENT'))
102
103
103 oid_handler.obj_size = 10240
104 oid_handler.obj_size = 10240
104 response, has_errors = oid_handler.exec_operation('upload')
105 response, has_errors = oid_handler.exec_operation('upload')
105 assert has_errors is None
106 assert has_errors is None
106 assert response['upload'] == {
107 assert response['upload'] == {
107 'header': {'Authorization': 'basic xxxx',
108 'header': {'Authorization': 'basic xxxx',
108 'Transfer-Encoding': 'chunked'},
109 'Transfer-Encoding': 'chunked'},
109 'href': 'http://localhost/handle_oid',
110 'href': 'http://localhost/handle_oid',
110 }
111 }
111
112
112 def test_upload_oid(self, oid_handler):
113 def test_upload_oid(self, oid_handler):
113 response, has_errors = oid_handler.exec_operation('upload')
114 response, has_errors = oid_handler.exec_operation('upload')
114 assert has_errors is None
115 assert has_errors is None
115 assert response['upload'] == {
116 assert response['upload'] == {
116 'header': {'Authorization': 'basic xxxx',
117 'header': {'Authorization': 'basic xxxx',
117 'Transfer-Encoding': 'chunked'},
118 'Transfer-Encoding': 'chunked'},
118 'href': 'http://localhost/handle_oid'
119 'href': 'http://localhost/handle_oid'
119 }
120 }
120
121
121
122
122 class TestLFSStore(object):
123 class TestLFSStore(object):
123 def test_write_oid(self, lfs_store):
124 def test_write_oid(self, lfs_store):
124 oid_location = lfs_store.oid_path
125 oid_location = lfs_store.oid_path
125
126
126 assert not os.path.isfile(oid_location)
127 assert not os.path.isfile(oid_location)
127
128
128 engine = lfs_store.get_engine(mode='wb')
129 engine = lfs_store.get_engine(mode='wb')
129 with engine as f:
130 with engine as f:
130 f.write('CONTENT')
131 f.write(safe_bytes('CONTENT'))
131
132
132 assert os.path.isfile(oid_location)
133 assert os.path.isfile(oid_location)
133
134
134 def test_detect_has_oid(self, lfs_store):
135 def test_detect_has_oid(self, lfs_store):
135
136
136 assert lfs_store.has_oid() is False
137 assert lfs_store.has_oid() is False
137 engine = lfs_store.get_engine(mode='wb')
138 engine = lfs_store.get_engine(mode='wb')
138 with engine as f:
139 with engine as f:
139 f.write('CONTENT')
140 f.write(safe_bytes('CONTENT'))
140
141
141 assert lfs_store.has_oid() is True No newline at end of file
142 assert lfs_store.has_oid() is True
@@ -1,205 +1,204 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # RhodeCode VCSServer provides access to different vcs backends via network.
3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2020 RhodeCode GmbH
4 # Copyright (C) 2014-2020 RhodeCode GmbH
5 #
5 #
6 # This program is free software; you can redistribute it and/or modify
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
9 # (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software Foundation,
17 # along with this program; if not, write to the Free Software Foundation,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
19
20 import re
20 import re
21 import os
21 import os
22 import sys
22 import sys
23 import datetime
23 import datetime
24 import logging
24 import logging
25 import pkg_resources
25 import pkg_resources
26
26
27 import vcsserver
27 import vcsserver
28 from vcsserver.utils import safe_bytes
28
29
29 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
30
31
31
32
32 def get_git_hooks_path(repo_path, bare):
33 def get_git_hooks_path(repo_path, bare):
33 hooks_path = os.path.join(repo_path, 'hooks')
34 hooks_path = os.path.join(repo_path, 'hooks')
34 if not bare:
35 if not bare:
35 hooks_path = os.path.join(repo_path, '.git', 'hooks')
36 hooks_path = os.path.join(repo_path, '.git', 'hooks')
36
37
37 return hooks_path
38 return hooks_path
38
39
39
40
40 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
41 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
41 """
42 """
42 Creates a RhodeCode hook inside a git repository
43 Creates a RhodeCode hook inside a git repository
43
44
44 :param repo_path: path to repository
45 :param repo_path: path to repository
45 :param executable: binary executable to put in the hooks
46 :param executable: binary executable to put in the hooks
46 :param force_create: Create even if same name hook exists
47 :param force_create: Create even if same name hook exists
47 """
48 """
48 executable = executable or sys.executable
49 executable = executable or sys.executable
49 hooks_path = get_git_hooks_path(repo_path, bare)
50 hooks_path = get_git_hooks_path(repo_path, bare)
50
51
51 if not os.path.isdir(hooks_path):
52 if not os.path.isdir(hooks_path):
52 os.makedirs(hooks_path, mode=0o777)
53 os.makedirs(hooks_path, mode=0o777)
53
54
54 tmpl_post = pkg_resources.resource_string(
55 tmpl_post = pkg_resources.resource_string(
55 'vcsserver', '/'.join(
56 'vcsserver', '/'.join(
56 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
57 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
57 tmpl_pre = pkg_resources.resource_string(
58 tmpl_pre = pkg_resources.resource_string(
58 'vcsserver', '/'.join(
59 'vcsserver', '/'.join(
59 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
60 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
60
61
61 path = '' # not used for now
62 path = '' # not used for now
62 timestamp = datetime.datetime.utcnow().isoformat()
63 timestamp = datetime.datetime.utcnow().isoformat()
63
64
64 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
65 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
65 log.debug('Installing git hook in repo %s', repo_path)
66 log.debug('Installing git hook in repo %s', repo_path)
66 _hook_file = os.path.join(hooks_path, '%s-receive' % h_type)
67 _hook_file = os.path.join(hooks_path, '%s-receive' % h_type)
67 _rhodecode_hook = check_rhodecode_hook(_hook_file)
68 _rhodecode_hook = check_rhodecode_hook(_hook_file)
68
69
69 if _rhodecode_hook or force_create:
70 if _rhodecode_hook or force_create:
70 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
71 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
71 try:
72 try:
72 with open(_hook_file, 'wb') as f:
73 with open(_hook_file, 'wb') as f:
73 template = template.replace(
74 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.__version__))
74 '_TMPL_', vcsserver.__version__)
75 template = template.replace(b'_DATE_', safe_bytes(timestamp))
75 template = template.replace('_DATE_', timestamp)
76 template = template.replace(b'_ENV_', safe_bytes(executable))
76 template = template.replace('_ENV_', executable)
77 template = template.replace(b'_PATH_', safe_bytes(path))
77 template = template.replace('_PATH_', path)
78 f.write(template)
78 f.write(template)
79 os.chmod(_hook_file, 0o755)
79 os.chmod(_hook_file, 0o755)
80 except IOError:
80 except IOError:
81 log.exception('error writing hook file %s', _hook_file)
81 log.exception('error writing hook file %s', _hook_file)
82 else:
82 else:
83 log.debug('skipping writing hook file')
83 log.debug('skipping writing hook file')
84
84
85 return True
85 return True
86
86
87
87
88 def get_svn_hooks_path(repo_path):
88 def get_svn_hooks_path(repo_path):
89 hooks_path = os.path.join(repo_path, 'hooks')
89 hooks_path = os.path.join(repo_path, 'hooks')
90
90
91 return hooks_path
91 return hooks_path
92
92
93
93
94 def install_svn_hooks(repo_path, executable=None, force_create=False):
94 def install_svn_hooks(repo_path, executable=None, force_create=False):
95 """
95 """
96 Creates RhodeCode hooks inside a svn repository
96 Creates RhodeCode hooks inside a svn repository
97
97
98 :param repo_path: path to repository
98 :param repo_path: path to repository
99 :param executable: binary executable to put in the hooks
99 :param executable: binary executable to put in the hooks
100 :param force_create: Create even if same name hook exists
100 :param force_create: Create even if same name hook exists
101 """
101 """
102 executable = executable or sys.executable
102 executable = executable or sys.executable
103 hooks_path = get_svn_hooks_path(repo_path)
103 hooks_path = get_svn_hooks_path(repo_path)
104 if not os.path.isdir(hooks_path):
104 if not os.path.isdir(hooks_path):
105 os.makedirs(hooks_path, mode=0o777)
105 os.makedirs(hooks_path, mode=0o777)
106
106
107 tmpl_post = pkg_resources.resource_string(
107 tmpl_post = pkg_resources.resource_string(
108 'vcsserver', '/'.join(
108 'vcsserver', '/'.join(
109 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
109 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
110 tmpl_pre = pkg_resources.resource_string(
110 tmpl_pre = pkg_resources.resource_string(
111 'vcsserver', '/'.join(
111 'vcsserver', '/'.join(
112 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
112 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
113
113
114 path = '' # not used for now
114 path = '' # not used for now
115 timestamp = datetime.datetime.utcnow().isoformat()
115 timestamp = datetime.datetime.utcnow().isoformat()
116
116
117 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
117 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
118 log.debug('Installing svn hook in repo %s', repo_path)
118 log.debug('Installing svn hook in repo %s', repo_path)
119 _hook_file = os.path.join(hooks_path, '%s-commit' % h_type)
119 _hook_file = os.path.join(hooks_path, '%s-commit' % h_type)
120 _rhodecode_hook = check_rhodecode_hook(_hook_file)
120 _rhodecode_hook = check_rhodecode_hook(_hook_file)
121
121
122 if _rhodecode_hook or force_create:
122 if _rhodecode_hook or force_create:
123 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
123 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
124
124
125 try:
125 try:
126 with open(_hook_file, 'wb') as f:
126 with open(_hook_file, 'wb') as f:
127 template = template.replace(
127 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.__version__))
128 '_TMPL_', vcsserver.__version__)
128 template = template.replace(b'_DATE_', safe_bytes(timestamp))
129 template = template.replace('_DATE_', timestamp)
129 template = template.replace(b'_ENV_', safe_bytes(executable))
130 template = template.replace('_ENV_', executable)
130 template = template.replace(b'_PATH_', safe_bytes(path))
131 template = template.replace('_PATH_', path)
132
131
133 f.write(template)
132 f.write(template)
134 os.chmod(_hook_file, 0o755)
133 os.chmod(_hook_file, 0o755)
135 except IOError:
134 except IOError:
136 log.exception('error writing hook file %s', _hook_file)
135 log.exception('error writing hook file %s', _hook_file)
137 else:
136 else:
138 log.debug('skipping writing hook file')
137 log.debug('skipping writing hook file')
139
138
140 return True
139 return True
141
140
142
141
143 def get_version_from_hook(hook_path):
142 def get_version_from_hook(hook_path):
144 version = ''
143 version = b''
145 hook_content = read_hook_content(hook_path)
144 hook_content = read_hook_content(hook_path)
146 matches = re.search(r'(?:RC_HOOK_VER)\s*=\s*(.*)', hook_content)
145 matches = re.search(rb'(?:RC_HOOK_VER)\s*=\s*(.*)', hook_content)
147 if matches:
146 if matches:
148 try:
147 try:
149 version = matches.groups()[0]
148 version = matches.groups()[0]
150 log.debug('got version %s from hooks.', version)
149 log.debug('got version %s from hooks.', version)
151 except Exception:
150 except Exception:
152 log.exception("Exception while reading the hook version.")
151 log.exception("Exception while reading the hook version.")
153 return version.replace("'", "")
152 return version.replace(b"'", b"")
154
153
155
154
156 def check_rhodecode_hook(hook_path):
155 def check_rhodecode_hook(hook_path):
157 """
156 """
158 Check if the hook was created by RhodeCode
157 Check if the hook was created by RhodeCode
159 """
158 """
160 if not os.path.exists(hook_path):
159 if not os.path.exists(hook_path):
161 return True
160 return True
162
161
163 log.debug('hook exists, checking if it is from RhodeCode')
162 log.debug('hook exists, checking if it is from RhodeCode')
164
163
165 version = get_version_from_hook(hook_path)
164 version = get_version_from_hook(hook_path)
166 if version:
165 if version:
167 return True
166 return True
168
167
169 return False
168 return False
170
169
171
170
172 def read_hook_content(hook_path):
171 def read_hook_content(hook_path):
173 content = ''
172 content = ''
174 if os.path.isfile(hook_path):
173 if os.path.isfile(hook_path):
175 with open(hook_path, 'rb') as f:
174 with open(hook_path, 'rb') as f:
176 content = f.read()
175 content = f.read()
177 return content
176 return content
178
177
179
178
180 def get_git_pre_hook_version(repo_path, bare):
179 def get_git_pre_hook_version(repo_path, bare):
181 hooks_path = get_git_hooks_path(repo_path, bare)
180 hooks_path = get_git_hooks_path(repo_path, bare)
182 _hook_file = os.path.join(hooks_path, 'pre-receive')
181 _hook_file = os.path.join(hooks_path, 'pre-receive')
183 version = get_version_from_hook(_hook_file)
182 version = get_version_from_hook(_hook_file)
184 return version
183 return version
185
184
186
185
187 def get_git_post_hook_version(repo_path, bare):
186 def get_git_post_hook_version(repo_path, bare):
188 hooks_path = get_git_hooks_path(repo_path, bare)
187 hooks_path = get_git_hooks_path(repo_path, bare)
189 _hook_file = os.path.join(hooks_path, 'post-receive')
188 _hook_file = os.path.join(hooks_path, 'post-receive')
190 version = get_version_from_hook(_hook_file)
189 version = get_version_from_hook(_hook_file)
191 return version
190 return version
192
191
193
192
194 def get_svn_pre_hook_version(repo_path):
193 def get_svn_pre_hook_version(repo_path):
195 hooks_path = get_svn_hooks_path(repo_path)
194 hooks_path = get_svn_hooks_path(repo_path)
196 _hook_file = os.path.join(hooks_path, 'pre-commit')
195 _hook_file = os.path.join(hooks_path, 'pre-commit')
197 version = get_version_from_hook(_hook_file)
196 version = get_version_from_hook(_hook_file)
198 return version
197 return version
199
198
200
199
201 def get_svn_post_hook_version(repo_path):
200 def get_svn_post_hook_version(repo_path):
202 hooks_path = get_svn_hooks_path(repo_path)
201 hooks_path = get_svn_hooks_path(repo_path)
203 _hook_file = os.path.join(hooks_path, 'post-commit')
202 _hook_file = os.path.join(hooks_path, 'post-commit')
204 version = get_version_from_hook(_hook_file)
203 version = get_version_from_hook(_hook_file)
205 return version
204 return version
@@ -1,729 +1,738 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # RhodeCode VCSServer provides access to different vcs backends via network.
3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2020 RhodeCode GmbH
4 # Copyright (C) 2014-2020 RhodeCode GmbH
5 #
5 #
6 # This program is free software; you can redistribute it and/or modify
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
9 # (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software Foundation,
17 # along with this program; if not, write to the Free Software Foundation,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
19
20 import io
20 import io
21 import os
21 import os
22 import sys
22 import sys
23 import logging
23 import logging
24 import collections
24 import collections
25 import importlib
25 import importlib
26 import base64
26 import base64
27 import msgpack
27
28
28 from http.client import HTTPConnection
29 from http.client import HTTPConnection
29
30
30
31
31 import mercurial.scmutil
32 import mercurial.scmutil
32 import mercurial.node
33 import mercurial.node
33 import simplejson as json
34
34
35 from vcsserver.lib.rc_json import json
35 from vcsserver import exceptions, subprocessio, settings
36 from vcsserver import exceptions, subprocessio, settings
37 from vcsserver.utils import safe_bytes
36
38
37 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
38
40
39
41
40 class HooksHttpClient(object):
42 class HooksHttpClient(object):
43 proto = 'msgpack.v1'
41 connection = None
44 connection = None
42
45
43 def __init__(self, hooks_uri):
46 def __init__(self, hooks_uri):
44 self.hooks_uri = hooks_uri
47 self.hooks_uri = hooks_uri
45
48
46 def __call__(self, method, extras):
49 def __call__(self, method, extras):
47 connection = HTTPConnection(self.hooks_uri)
50 connection = HTTPConnection(self.hooks_uri)
48 body = self._serialize(method, extras)
51 # binary msgpack body
52 headers, body = self._serialize(method, extras)
49 try:
53 try:
50 connection.request('POST', '/', body)
54 connection.request('POST', '/', body, headers)
51 except Exception:
55 except Exception as error:
52 log.error('Hooks calling Connection failed on %s', connection.__dict__)
56 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
53 raise
57 raise
54 response = connection.getresponse()
58 response = connection.getresponse()
55
56 response_data = response.read()
57
58 try:
59 try:
59 return json.loads(response_data)
60 return msgpack.load(response, raw=False)
60 except Exception:
61 except Exception:
62 response_data = response.read()
61 log.exception('Failed to decode hook response json data. '
63 log.exception('Failed to decode hook response json data. '
62 'response_code:%s, raw_data:%s',
64 'response_code:%s, raw_data:%s',
63 response.status, response_data)
65 response.status, response_data)
64 raise
66 raise
65
67
66 def _serialize(self, hook_name, extras):
68 @classmethod
69 def _serialize(cls, hook_name, extras):
67 data = {
70 data = {
68 'method': hook_name,
71 'method': hook_name,
69 'extras': extras
72 'extras': extras
70 }
73 }
71 return json.dumps(data)
74 headers = {
75 'rc-hooks-protocol': cls.proto
76 }
77 return headers, msgpack.packb(data)
72
78
73
79
74 class HooksDummyClient(object):
80 class HooksDummyClient(object):
75 def __init__(self, hooks_module):
81 def __init__(self, hooks_module):
76 self._hooks_module = importlib.import_module(hooks_module)
82 self._hooks_module = importlib.import_module(hooks_module)
77
83
78 def __call__(self, hook_name, extras):
84 def __call__(self, hook_name, extras):
79 with self._hooks_module.Hooks() as hooks:
85 with self._hooks_module.Hooks() as hooks:
80 return getattr(hooks, hook_name)(extras)
86 return getattr(hooks, hook_name)(extras)
81
87
82
88
83 class HooksShadowRepoClient(object):
89 class HooksShadowRepoClient(object):
84
90
85 def __call__(self, hook_name, extras):
91 def __call__(self, hook_name, extras):
86 return {'output': '', 'status': 0}
92 return {'output': '', 'status': 0}
87
93
88
94
89 class RemoteMessageWriter(object):
95 class RemoteMessageWriter(object):
90 """Writer base class."""
96 """Writer base class."""
91 def write(self, message):
97 def write(self, message):
92 raise NotImplementedError()
98 raise NotImplementedError()
93
99
94
100
95 class HgMessageWriter(RemoteMessageWriter):
101 class HgMessageWriter(RemoteMessageWriter):
96 """Writer that knows how to send messages to mercurial clients."""
102 """Writer that knows how to send messages to mercurial clients."""
97
103
98 def __init__(self, ui):
104 def __init__(self, ui):
99 self.ui = ui
105 self.ui = ui
100
106
101 def write(self, message):
107 def write(self, message):
102 # TODO: Check why the quiet flag is set by default.
108 # TODO: Check why the quiet flag is set by default.
103 old = self.ui.quiet
109 old = self.ui.quiet
104 self.ui.quiet = False
110 self.ui.quiet = False
105 self.ui.status(message.encode('utf-8'))
111 self.ui.status(message.encode('utf-8'))
106 self.ui.quiet = old
112 self.ui.quiet = old
107
113
108
114
109 class GitMessageWriter(RemoteMessageWriter):
115 class GitMessageWriter(RemoteMessageWriter):
110 """Writer that knows how to send messages to git clients."""
116 """Writer that knows how to send messages to git clients."""
111
117
112 def __init__(self, stdout=None):
118 def __init__(self, stdout=None):
113 self.stdout = stdout or sys.stdout
119 self.stdout = stdout or sys.stdout
114
120
115 def write(self, message):
121 def write(self, message):
116 self.stdout.write(message.encode('utf-8'))
122 self.stdout.write(safe_bytes(message))
117
123
118
124
119 class SvnMessageWriter(RemoteMessageWriter):
125 class SvnMessageWriter(RemoteMessageWriter):
120 """Writer that knows how to send messages to svn clients."""
126 """Writer that knows how to send messages to svn clients."""
121
127
122 def __init__(self, stderr=None):
128 def __init__(self, stderr=None):
123 # SVN needs data sent to stderr for back-to-client messaging
129 # SVN needs data sent to stderr for back-to-client messaging
124 self.stderr = stderr or sys.stderr
130 self.stderr = stderr or sys.stderr
125
131
126 def write(self, message):
132 def write(self, message):
127 self.stderr.write(message.encode('utf-8'))
133 self.stderr.write(message.encode('utf-8'))
128
134
129
135
130 def _handle_exception(result):
136 def _handle_exception(result):
131 exception_class = result.get('exception')
137 exception_class = result.get('exception')
132 exception_traceback = result.get('exception_traceback')
138 exception_traceback = result.get('exception_traceback')
133
139
134 if exception_traceback:
140 if exception_traceback:
135 log.error('Got traceback from remote call:%s', exception_traceback)
141 log.error('Got traceback from remote call:%s', exception_traceback)
136
142
137 if exception_class == 'HTTPLockedRC':
143 if exception_class == 'HTTPLockedRC':
138 raise exceptions.RepositoryLockedException()(*result['exception_args'])
144 raise exceptions.RepositoryLockedException()(*result['exception_args'])
139 elif exception_class == 'HTTPBranchProtected':
145 elif exception_class == 'HTTPBranchProtected':
140 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
146 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
141 elif exception_class == 'RepositoryError':
147 elif exception_class == 'RepositoryError':
142 raise exceptions.VcsException()(*result['exception_args'])
148 raise exceptions.VcsException()(*result['exception_args'])
143 elif exception_class:
149 elif exception_class:
144 raise Exception('Got remote exception "%s" with args "%s"' %
150 raise Exception('Got remote exception "%s" with args "%s"' %
145 (exception_class, result['exception_args']))
151 (exception_class, result['exception_args']))
146
152
147
153
148 def _get_hooks_client(extras):
154 def _get_hooks_client(extras):
149 hooks_uri = extras.get('hooks_uri')
155 hooks_uri = extras.get('hooks_uri')
150 is_shadow_repo = extras.get('is_shadow_repo')
156 is_shadow_repo = extras.get('is_shadow_repo')
151 if hooks_uri:
157 if hooks_uri:
152 return HooksHttpClient(extras['hooks_uri'])
158 return HooksHttpClient(extras['hooks_uri'])
153 elif is_shadow_repo:
159 elif is_shadow_repo:
154 return HooksShadowRepoClient()
160 return HooksShadowRepoClient()
155 else:
161 else:
156 return HooksDummyClient(extras['hooks_module'])
162 return HooksDummyClient(extras['hooks_module'])
157
163
158
164
159 def _call_hook(hook_name, extras, writer):
165 def _call_hook(hook_name, extras, writer):
160 hooks_client = _get_hooks_client(extras)
166 hooks_client = _get_hooks_client(extras)
161 log.debug('Hooks, using client:%s', hooks_client)
167 log.debug('Hooks, using client:%s', hooks_client)
162 result = hooks_client(hook_name, extras)
168 result = hooks_client(hook_name, extras)
163 log.debug('Hooks got result: %s', result)
169 log.debug('Hooks got result: %s', result)
164
170
165 _handle_exception(result)
171 _handle_exception(result)
166 writer.write(result['output'])
172 writer.write(result['output'])
167
173
168 return result['status']
174 return result['status']
169
175
170
176
171 def _extras_from_ui(ui):
177 def _extras_from_ui(ui):
172 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
178 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
173 if not hook_data:
179 if not hook_data:
174 # maybe it's inside environ ?
180 # maybe it's inside environ ?
175 env_hook_data = os.environ.get('RC_SCM_DATA')
181 env_hook_data = os.environ.get('RC_SCM_DATA')
176 if env_hook_data:
182 if env_hook_data:
177 hook_data = env_hook_data
183 hook_data = env_hook_data
178
184
179 extras = {}
185 extras = {}
180 if hook_data:
186 if hook_data:
181 extras = json.loads(hook_data)
187 extras = json.loads(hook_data)
182 return extras
188 return extras
183
189
184
190
185 def _rev_range_hash(repo, node, check_heads=False):
191 def _rev_range_hash(repo, node, check_heads=False):
186 from vcsserver.hgcompat import get_ctx
192 from vcsserver.hgcompat import get_ctx
187
193
188 commits = []
194 commits = []
189 revs = []
195 revs = []
190 start = get_ctx(repo, node).rev()
196 start = get_ctx(repo, node).rev()
191 end = len(repo)
197 end = len(repo)
192 for rev in range(start, end):
198 for rev in range(start, end):
193 revs.append(rev)
199 revs.append(rev)
194 ctx = get_ctx(repo, rev)
200 ctx = get_ctx(repo, rev)
195 commit_id = mercurial.node.hex(ctx.node())
201 commit_id = mercurial.node.hex(ctx.node())
196 branch = ctx.branch()
202 branch = ctx.branch()
197 commits.append((commit_id, branch))
203 commits.append((commit_id, branch))
198
204
199 parent_heads = []
205 parent_heads = []
200 if check_heads:
206 if check_heads:
201 parent_heads = _check_heads(repo, start, end, revs)
207 parent_heads = _check_heads(repo, start, end, revs)
202 return commits, parent_heads
208 return commits, parent_heads
203
209
204
210
205 def _check_heads(repo, start, end, commits):
211 def _check_heads(repo, start, end, commits):
206 from vcsserver.hgcompat import get_ctx
212 from vcsserver.hgcompat import get_ctx
207 changelog = repo.changelog
213 changelog = repo.changelog
208 parents = set()
214 parents = set()
209
215
210 for new_rev in commits:
216 for new_rev in commits:
211 for p in changelog.parentrevs(new_rev):
217 for p in changelog.parentrevs(new_rev):
212 if p == mercurial.node.nullrev:
218 if p == mercurial.node.nullrev:
213 continue
219 continue
214 if p < start:
220 if p < start:
215 parents.add(p)
221 parents.add(p)
216
222
217 for p in parents:
223 for p in parents:
218 branch = get_ctx(repo, p).branch()
224 branch = get_ctx(repo, p).branch()
219 # The heads descending from that parent, on the same branch
225 # The heads descending from that parent, on the same branch
220 parent_heads = set([p])
226 parent_heads = set([p])
221 reachable = set([p])
227 reachable = set([p])
222 for x in range(p + 1, end):
228 for x in range(p + 1, end):
223 if get_ctx(repo, x).branch() != branch:
229 if get_ctx(repo, x).branch() != branch:
224 continue
230 continue
225 for pp in changelog.parentrevs(x):
231 for pp in changelog.parentrevs(x):
226 if pp in reachable:
232 if pp in reachable:
227 reachable.add(x)
233 reachable.add(x)
228 parent_heads.discard(pp)
234 parent_heads.discard(pp)
229 parent_heads.add(x)
235 parent_heads.add(x)
230 # More than one head? Suggest merging
236 # More than one head? Suggest merging
231 if len(parent_heads) > 1:
237 if len(parent_heads) > 1:
232 return list(parent_heads)
238 return list(parent_heads)
233
239
234 return []
240 return []
235
241
236
242
237 def _get_git_env():
243 def _get_git_env():
238 env = {}
244 env = {}
239 for k, v in os.environ.items():
245 for k, v in os.environ.items():
240 if k.startswith('GIT'):
246 if k.startswith('GIT'):
241 env[k] = v
247 env[k] = v
242
248
243 # serialized version
249 # serialized version
244 return [(k, v) for k, v in env.items()]
250 return [(k, v) for k, v in env.items()]
245
251
246
252
247 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
253 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
248 env = {}
254 env = {}
249 for k, v in os.environ.items():
255 for k, v in os.environ.items():
250 if k.startswith('HG'):
256 if k.startswith('HG'):
251 env[k] = v
257 env[k] = v
252
258
253 env['HG_NODE'] = old_rev
259 env['HG_NODE'] = old_rev
254 env['HG_NODE_LAST'] = new_rev
260 env['HG_NODE_LAST'] = new_rev
255 env['HG_TXNID'] = txnid
261 env['HG_TXNID'] = txnid
256 env['HG_PENDING'] = repo_path
262 env['HG_PENDING'] = repo_path
257
263
258 return [(k, v) for k, v in env.items()]
264 return [(k, v) for k, v in env.items()]
259
265
260
266
261 def repo_size(ui, repo, **kwargs):
267 def repo_size(ui, repo, **kwargs):
262 extras = _extras_from_ui(ui)
268 extras = _extras_from_ui(ui)
263 return _call_hook('repo_size', extras, HgMessageWriter(ui))
269 return _call_hook('repo_size', extras, HgMessageWriter(ui))
264
270
265
271
266 def pre_pull(ui, repo, **kwargs):
272 def pre_pull(ui, repo, **kwargs):
267 extras = _extras_from_ui(ui)
273 extras = _extras_from_ui(ui)
268 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
274 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
269
275
270
276
271 def pre_pull_ssh(ui, repo, **kwargs):
277 def pre_pull_ssh(ui, repo, **kwargs):
272 extras = _extras_from_ui(ui)
278 extras = _extras_from_ui(ui)
273 if extras and extras.get('SSH'):
279 if extras and extras.get('SSH'):
274 return pre_pull(ui, repo, **kwargs)
280 return pre_pull(ui, repo, **kwargs)
275 return 0
281 return 0
276
282
277
283
278 def post_pull(ui, repo, **kwargs):
284 def post_pull(ui, repo, **kwargs):
279 extras = _extras_from_ui(ui)
285 extras = _extras_from_ui(ui)
280 return _call_hook('post_pull', extras, HgMessageWriter(ui))
286 return _call_hook('post_pull', extras, HgMessageWriter(ui))
281
287
282
288
283 def post_pull_ssh(ui, repo, **kwargs):
289 def post_pull_ssh(ui, repo, **kwargs):
284 extras = _extras_from_ui(ui)
290 extras = _extras_from_ui(ui)
285 if extras and extras.get('SSH'):
291 if extras and extras.get('SSH'):
286 return post_pull(ui, repo, **kwargs)
292 return post_pull(ui, repo, **kwargs)
287 return 0
293 return 0
288
294
289
295
290 def pre_push(ui, repo, node=None, **kwargs):
296 def pre_push(ui, repo, node=None, **kwargs):
291 """
297 """
292 Mercurial pre_push hook
298 Mercurial pre_push hook
293 """
299 """
294 extras = _extras_from_ui(ui)
300 extras = _extras_from_ui(ui)
295 detect_force_push = extras.get('detect_force_push')
301 detect_force_push = extras.get('detect_force_push')
296
302
297 rev_data = []
303 rev_data = []
298 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
304 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
299 branches = collections.defaultdict(list)
305 branches = collections.defaultdict(list)
300 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
306 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
301 for commit_id, branch in commits:
307 for commit_id, branch in commits:
302 branches[branch].append(commit_id)
308 branches[branch].append(commit_id)
303
309
304 for branch, commits in branches.items():
310 for branch, commits in branches.items():
305 old_rev = kwargs.get('node_last') or commits[0]
311 old_rev = kwargs.get('node_last') or commits[0]
306 rev_data.append({
312 rev_data.append({
307 'total_commits': len(commits),
313 'total_commits': len(commits),
308 'old_rev': old_rev,
314 'old_rev': old_rev,
309 'new_rev': commits[-1],
315 'new_rev': commits[-1],
310 'ref': '',
316 'ref': '',
311 'type': 'branch',
317 'type': 'branch',
312 'name': branch,
318 'name': branch,
313 })
319 })
314
320
315 for push_ref in rev_data:
321 for push_ref in rev_data:
316 push_ref['multiple_heads'] = _heads
322 push_ref['multiple_heads'] = _heads
317
323
318 repo_path = os.path.join(
324 repo_path = os.path.join(
319 extras.get('repo_store', ''), extras.get('repository', ''))
325 extras.get('repo_store', ''), extras.get('repository', ''))
320 push_ref['hg_env'] = _get_hg_env(
326 push_ref['hg_env'] = _get_hg_env(
321 old_rev=push_ref['old_rev'],
327 old_rev=push_ref['old_rev'],
322 new_rev=push_ref['new_rev'], txnid=kwargs.get('txnid'),
328 new_rev=push_ref['new_rev'], txnid=kwargs.get('txnid'),
323 repo_path=repo_path)
329 repo_path=repo_path)
324
330
325 extras['hook_type'] = kwargs.get('hooktype', 'pre_push')
331 extras['hook_type'] = kwargs.get('hooktype', 'pre_push')
326 extras['commit_ids'] = rev_data
332 extras['commit_ids'] = rev_data
327
333
328 return _call_hook('pre_push', extras, HgMessageWriter(ui))
334 return _call_hook('pre_push', extras, HgMessageWriter(ui))
329
335
330
336
331 def pre_push_ssh(ui, repo, node=None, **kwargs):
337 def pre_push_ssh(ui, repo, node=None, **kwargs):
332 extras = _extras_from_ui(ui)
338 extras = _extras_from_ui(ui)
333 if extras.get('SSH'):
339 if extras.get('SSH'):
334 return pre_push(ui, repo, node, **kwargs)
340 return pre_push(ui, repo, node, **kwargs)
335
341
336 return 0
342 return 0
337
343
338
344
339 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
345 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
340 """
346 """
341 Mercurial pre_push hook for SSH
347 Mercurial pre_push hook for SSH
342 """
348 """
343 extras = _extras_from_ui(ui)
349 extras = _extras_from_ui(ui)
344 if extras.get('SSH'):
350 if extras.get('SSH'):
345 permission = extras['SSH_PERMISSIONS']
351 permission = extras['SSH_PERMISSIONS']
346
352
347 if 'repository.write' == permission or 'repository.admin' == permission:
353 if 'repository.write' == permission or 'repository.admin' == permission:
348 return 0
354 return 0
349
355
350 # non-zero ret code
356 # non-zero ret code
351 return 1
357 return 1
352
358
353 return 0
359 return 0
354
360
355
361
356 def post_push(ui, repo, node, **kwargs):
362 def post_push(ui, repo, node, **kwargs):
357 """
363 """
358 Mercurial post_push hook
364 Mercurial post_push hook
359 """
365 """
360 extras = _extras_from_ui(ui)
366 extras = _extras_from_ui(ui)
361
367
362 commit_ids = []
368 commit_ids = []
363 branches = []
369 branches = []
364 bookmarks = []
370 bookmarks = []
365 tags = []
371 tags = []
366
372
367 commits, _heads = _rev_range_hash(repo, node)
373 commits, _heads = _rev_range_hash(repo, node)
368 for commit_id, branch in commits:
374 for commit_id, branch in commits:
369 commit_ids.append(commit_id)
375 commit_ids.append(commit_id)
370 if branch not in branches:
376 if branch not in branches:
371 branches.append(branch)
377 branches.append(branch)
372
378
373 if hasattr(ui, '_rc_pushkey_branches'):
379 if hasattr(ui, '_rc_pushkey_branches'):
374 bookmarks = ui._rc_pushkey_branches
380 bookmarks = ui._rc_pushkey_branches
375
381
376 extras['hook_type'] = kwargs.get('hooktype', 'post_push')
382 extras['hook_type'] = kwargs.get('hooktype', 'post_push')
377 extras['commit_ids'] = commit_ids
383 extras['commit_ids'] = commit_ids
378 extras['new_refs'] = {
384 extras['new_refs'] = {
379 'branches': branches,
385 'branches': branches,
380 'bookmarks': bookmarks,
386 'bookmarks': bookmarks,
381 'tags': tags
387 'tags': tags
382 }
388 }
383
389
384 return _call_hook('post_push', extras, HgMessageWriter(ui))
390 return _call_hook('post_push', extras, HgMessageWriter(ui))
385
391
386
392
387 def post_push_ssh(ui, repo, node, **kwargs):
393 def post_push_ssh(ui, repo, node, **kwargs):
388 """
394 """
389 Mercurial post_push hook for SSH
395 Mercurial post_push hook for SSH
390 """
396 """
391 if _extras_from_ui(ui).get('SSH'):
397 if _extras_from_ui(ui).get('SSH'):
392 return post_push(ui, repo, node, **kwargs)
398 return post_push(ui, repo, node, **kwargs)
393 return 0
399 return 0
394
400
395
401
396 def key_push(ui, repo, **kwargs):
402 def key_push(ui, repo, **kwargs):
397 from vcsserver.hgcompat import get_ctx
403 from vcsserver.hgcompat import get_ctx
398 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
404 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
399 # store new bookmarks in our UI object propagated later to post_push
405 # store new bookmarks in our UI object propagated later to post_push
400 ui._rc_pushkey_branches = get_ctx(repo, kwargs['key']).bookmarks()
406 ui._rc_pushkey_branches = get_ctx(repo, kwargs['key']).bookmarks()
401 return
407 return
402
408
403
409
404 # backward compat
410 # backward compat
405 log_pull_action = post_pull
411 log_pull_action = post_pull
406
412
407 # backward compat
413 # backward compat
408 log_push_action = post_push
414 log_push_action = post_push
409
415
410
416
411 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
417 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
412 """
418 """
413 Old hook name: keep here for backward compatibility.
419 Old hook name: keep here for backward compatibility.
414
420
415 This is only required when the installed git hooks are not upgraded.
421 This is only required when the installed git hooks are not upgraded.
416 """
422 """
417 pass
423 pass
418
424
419
425
420 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
426 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
421 """
427 """
422 Old hook name: keep here for backward compatibility.
428 Old hook name: keep here for backward compatibility.
423
429
424 This is only required when the installed git hooks are not upgraded.
430 This is only required when the installed git hooks are not upgraded.
425 """
431 """
426 pass
432 pass
427
433
428
434
429 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
435 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
430
436
431
437
432 def git_pre_pull(extras):
438 def git_pre_pull(extras):
433 """
439 """
434 Pre pull hook.
440 Pre pull hook.
435
441
436 :param extras: dictionary containing the keys defined in simplevcs
442 :param extras: dictionary containing the keys defined in simplevcs
437 :type extras: dict
443 :type extras: dict
438
444
439 :return: status code of the hook. 0 for success.
445 :return: status code of the hook. 0 for success.
440 :rtype: int
446 :rtype: int
441 """
447 """
448
442 if 'pull' not in extras['hooks']:
449 if 'pull' not in extras['hooks']:
443 return HookResponse(0, '')
450 return HookResponse(0, '')
444
451
445 stdout = io.BytesIO()
452 stdout = io.BytesIO()
446 try:
453 try:
447 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
454 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
455
448 except Exception as error:
456 except Exception as error:
457 log.exception('Failed to call pre_pull hook')
449 status = 128
458 status = 128
450 stdout.write('ERROR: %s\n' % str(error))
459 stdout.write(safe_bytes(f'ERROR: {error}\n'))
451
460
452 return HookResponse(status, stdout.getvalue())
461 return HookResponse(status, stdout.getvalue())
453
462
454
463
455 def git_post_pull(extras):
464 def git_post_pull(extras):
456 """
465 """
457 Post pull hook.
466 Post pull hook.
458
467
459 :param extras: dictionary containing the keys defined in simplevcs
468 :param extras: dictionary containing the keys defined in simplevcs
460 :type extras: dict
469 :type extras: dict
461
470
462 :return: status code of the hook. 0 for success.
471 :return: status code of the hook. 0 for success.
463 :rtype: int
472 :rtype: int
464 """
473 """
465 if 'pull' not in extras['hooks']:
474 if 'pull' not in extras['hooks']:
466 return HookResponse(0, '')
475 return HookResponse(0, '')
467
476
468 stdout = io.BytesIO()
477 stdout = io.BytesIO()
469 try:
478 try:
470 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
479 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
471 except Exception as error:
480 except Exception as error:
472 status = 128
481 status = 128
473 stdout.write('ERROR: %s\n' % error)
482 stdout.write(safe_bytes(f'ERROR: {error}\n'))
474
483
475 return HookResponse(status, stdout.getvalue())
484 return HookResponse(status, stdout.getvalue())
476
485
477
486
478 def _parse_git_ref_lines(revision_lines):
487 def _parse_git_ref_lines(revision_lines):
479 rev_data = []
488 rev_data = []
480 for revision_line in revision_lines or []:
489 for revision_line in revision_lines or []:
481 old_rev, new_rev, ref = revision_line.strip().split(' ')
490 old_rev, new_rev, ref = revision_line.strip().split(' ')
482 ref_data = ref.split('/', 2)
491 ref_data = ref.split('/', 2)
483 if ref_data[1] in ('tags', 'heads'):
492 if ref_data[1] in ('tags', 'heads'):
484 rev_data.append({
493 rev_data.append({
485 # NOTE(marcink):
494 # NOTE(marcink):
486 # we're unable to tell total_commits for git at this point
495 # we're unable to tell total_commits for git at this point
487 # but we set the variable for consistency with GIT
496 # but we set the variable for consistency with GIT
488 'total_commits': -1,
497 'total_commits': -1,
489 'old_rev': old_rev,
498 'old_rev': old_rev,
490 'new_rev': new_rev,
499 'new_rev': new_rev,
491 'ref': ref,
500 'ref': ref,
492 'type': ref_data[1],
501 'type': ref_data[1],
493 'name': ref_data[2],
502 'name': ref_data[2],
494 })
503 })
495 return rev_data
504 return rev_data
496
505
497
506
498 def git_pre_receive(unused_repo_path, revision_lines, env):
507 def git_pre_receive(unused_repo_path, revision_lines, env):
499 """
508 """
500 Pre push hook.
509 Pre push hook.
501
510
502 :param extras: dictionary containing the keys defined in simplevcs
511 :param extras: dictionary containing the keys defined in simplevcs
503 :type extras: dict
512 :type extras: dict
504
513
505 :return: status code of the hook. 0 for success.
514 :return: status code of the hook. 0 for success.
506 :rtype: int
515 :rtype: int
507 """
516 """
508 extras = json.loads(env['RC_SCM_DATA'])
517 extras = json.loads(env['RC_SCM_DATA'])
509 rev_data = _parse_git_ref_lines(revision_lines)
518 rev_data = _parse_git_ref_lines(revision_lines)
510 if 'push' not in extras['hooks']:
519 if 'push' not in extras['hooks']:
511 return 0
520 return 0
512 empty_commit_id = '0' * 40
521 empty_commit_id = '0' * 40
513
522
514 detect_force_push = extras.get('detect_force_push')
523 detect_force_push = extras.get('detect_force_push')
515
524
516 for push_ref in rev_data:
525 for push_ref in rev_data:
517 # store our git-env which holds the temp store
526 # store our git-env which holds the temp store
518 push_ref['git_env'] = _get_git_env()
527 push_ref['git_env'] = _get_git_env()
519 push_ref['pruned_sha'] = ''
528 push_ref['pruned_sha'] = ''
520 if not detect_force_push:
529 if not detect_force_push:
521 # don't check for forced-push when we don't need to
530 # don't check for forced-push when we don't need to
522 continue
531 continue
523
532
524 type_ = push_ref['type']
533 type_ = push_ref['type']
525 new_branch = push_ref['old_rev'] == empty_commit_id
534 new_branch = push_ref['old_rev'] == empty_commit_id
526 delete_branch = push_ref['new_rev'] == empty_commit_id
535 delete_branch = push_ref['new_rev'] == empty_commit_id
527 if type_ == 'heads' and not (new_branch or delete_branch):
536 if type_ == 'heads' and not (new_branch or delete_branch):
528 old_rev = push_ref['old_rev']
537 old_rev = push_ref['old_rev']
529 new_rev = push_ref['new_rev']
538 new_rev = push_ref['new_rev']
530 cmd = [settings.GIT_EXECUTABLE, 'rev-list', old_rev, '^{}'.format(new_rev)]
539 cmd = [settings.GIT_EXECUTABLE, 'rev-list', old_rev, '^{}'.format(new_rev)]
531 stdout, stderr = subprocessio.run_command(
540 stdout, stderr = subprocessio.run_command(
532 cmd, env=os.environ.copy())
541 cmd, env=os.environ.copy())
533 # means we're having some non-reachable objects, this forced push was used
542 # means we're having some non-reachable objects, this forced push was used
534 if stdout:
543 if stdout:
535 push_ref['pruned_sha'] = stdout.splitlines()
544 push_ref['pruned_sha'] = stdout.splitlines()
536
545
537 extras['hook_type'] = 'pre_receive'
546 extras['hook_type'] = 'pre_receive'
538 extras['commit_ids'] = rev_data
547 extras['commit_ids'] = rev_data
539 return _call_hook('pre_push', extras, GitMessageWriter())
548 return _call_hook('pre_push', extras, GitMessageWriter())
540
549
541
550
542 def git_post_receive(unused_repo_path, revision_lines, env):
551 def git_post_receive(unused_repo_path, revision_lines, env):
543 """
552 """
544 Post push hook.
553 Post push hook.
545
554
546 :param extras: dictionary containing the keys defined in simplevcs
555 :param extras: dictionary containing the keys defined in simplevcs
547 :type extras: dict
556 :type extras: dict
548
557
549 :return: status code of the hook. 0 for success.
558 :return: status code of the hook. 0 for success.
550 :rtype: int
559 :rtype: int
551 """
560 """
552 extras = json.loads(env['RC_SCM_DATA'])
561 extras = json.loads(env['RC_SCM_DATA'])
553 if 'push' not in extras['hooks']:
562 if 'push' not in extras['hooks']:
554 return 0
563 return 0
555
564
556 rev_data = _parse_git_ref_lines(revision_lines)
565 rev_data = _parse_git_ref_lines(revision_lines)
557
566
558 git_revs = []
567 git_revs = []
559
568
560 # N.B.(skreft): it is ok to just call git, as git before calling a
569 # N.B.(skreft): it is ok to just call git, as git before calling a
561 # subcommand sets the PATH environment variable so that it point to the
570 # subcommand sets the PATH environment variable so that it point to the
562 # correct version of the git executable.
571 # correct version of the git executable.
563 empty_commit_id = '0' * 40
572 empty_commit_id = '0' * 40
564 branches = []
573 branches = []
565 tags = []
574 tags = []
566 for push_ref in rev_data:
575 for push_ref in rev_data:
567 type_ = push_ref['type']
576 type_ = push_ref['type']
568
577
569 if type_ == 'heads':
578 if type_ == 'heads':
570 if push_ref['old_rev'] == empty_commit_id:
579 if push_ref['old_rev'] == empty_commit_id:
571 # starting new branch case
580 # starting new branch case
572 if push_ref['name'] not in branches:
581 if push_ref['name'] not in branches:
573 branches.append(push_ref['name'])
582 branches.append(push_ref['name'])
574
583
575 # Fix up head revision if needed
584 # Fix up head revision if needed
576 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
585 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
577 try:
586 try:
578 subprocessio.run_command(cmd, env=os.environ.copy())
587 subprocessio.run_command(cmd, env=os.environ.copy())
579 except Exception:
588 except Exception:
580 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', '"HEAD"',
589 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', '"HEAD"',
581 '"refs/heads/%s"' % push_ref['name']]
590 '"refs/heads/%s"' % push_ref['name']]
582 print(("Setting default branch to %s" % push_ref['name']))
591 print(("Setting default branch to %s" % push_ref['name']))
583 subprocessio.run_command(cmd, env=os.environ.copy())
592 subprocessio.run_command(cmd, env=os.environ.copy())
584
593
585 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
594 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
586 '--format=%(refname)', 'refs/heads/*']
595 '--format=%(refname)', 'refs/heads/*']
587 stdout, stderr = subprocessio.run_command(
596 stdout, stderr = subprocessio.run_command(
588 cmd, env=os.environ.copy())
597 cmd, env=os.environ.copy())
589 heads = stdout
598 heads = stdout
590 heads = heads.replace(push_ref['ref'], '')
599 heads = heads.replace(push_ref['ref'], '')
591 heads = ' '.join(head for head
600 heads = ' '.join(head for head
592 in heads.splitlines() if head) or '.'
601 in heads.splitlines() if head) or '.'
593 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
602 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
594 '--pretty=format:%H', '--', push_ref['new_rev'],
603 '--pretty=format:%H', '--', push_ref['new_rev'],
595 '--not', heads]
604 '--not', heads]
596 stdout, stderr = subprocessio.run_command(
605 stdout, stderr = subprocessio.run_command(
597 cmd, env=os.environ.copy())
606 cmd, env=os.environ.copy())
598 git_revs.extend(stdout.splitlines())
607 git_revs.extend(stdout.splitlines())
599 elif push_ref['new_rev'] == empty_commit_id:
608 elif push_ref['new_rev'] == empty_commit_id:
600 # delete branch case
609 # delete branch case
601 git_revs.append('delete_branch=>%s' % push_ref['name'])
610 git_revs.append('delete_branch=>%s' % push_ref['name'])
602 else:
611 else:
603 if push_ref['name'] not in branches:
612 if push_ref['name'] not in branches:
604 branches.append(push_ref['name'])
613 branches.append(push_ref['name'])
605
614
606 cmd = [settings.GIT_EXECUTABLE, 'log',
615 cmd = [settings.GIT_EXECUTABLE, 'log',
607 '{old_rev}..{new_rev}'.format(**push_ref),
616 '{old_rev}..{new_rev}'.format(**push_ref),
608 '--reverse', '--pretty=format:%H']
617 '--reverse', '--pretty=format:%H']
609 stdout, stderr = subprocessio.run_command(
618 stdout, stderr = subprocessio.run_command(
610 cmd, env=os.environ.copy())
619 cmd, env=os.environ.copy())
611 git_revs.extend(stdout.splitlines())
620 git_revs.extend(stdout.splitlines())
612 elif type_ == 'tags':
621 elif type_ == 'tags':
613 if push_ref['name'] not in tags:
622 if push_ref['name'] not in tags:
614 tags.append(push_ref['name'])
623 tags.append(push_ref['name'])
615 git_revs.append('tag=>%s' % push_ref['name'])
624 git_revs.append('tag=>%s' % push_ref['name'])
616
625
617 extras['hook_type'] = 'post_receive'
626 extras['hook_type'] = 'post_receive'
618 extras['commit_ids'] = git_revs
627 extras['commit_ids'] = git_revs
619 extras['new_refs'] = {
628 extras['new_refs'] = {
620 'branches': branches,
629 'branches': branches,
621 'bookmarks': [],
630 'bookmarks': [],
622 'tags': tags,
631 'tags': tags,
623 }
632 }
624
633
625 if 'repo_size' in extras['hooks']:
634 if 'repo_size' in extras['hooks']:
626 try:
635 try:
627 _call_hook('repo_size', extras, GitMessageWriter())
636 _call_hook('repo_size', extras, GitMessageWriter())
628 except:
637 except:
629 pass
638 pass
630
639
631 return _call_hook('post_push', extras, GitMessageWriter())
640 return _call_hook('post_push', extras, GitMessageWriter())
632
641
633
642
634 def _get_extras_from_txn_id(path, txn_id):
643 def _get_extras_from_txn_id(path, txn_id):
635 extras = {}
644 extras = {}
636 try:
645 try:
637 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
646 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
638 '-t', txn_id,
647 '-t', txn_id,
639 '--revprop', path, 'rc-scm-extras']
648 '--revprop', path, 'rc-scm-extras']
640 stdout, stderr = subprocessio.run_command(
649 stdout, stderr = subprocessio.run_command(
641 cmd, env=os.environ.copy())
650 cmd, env=os.environ.copy())
642 extras = json.loads(base64.urlsafe_b64decode(stdout))
651 extras = json.loads(base64.urlsafe_b64decode(stdout))
643 except Exception:
652 except Exception:
644 log.exception('Failed to extract extras info from txn_id')
653 log.exception('Failed to extract extras info from txn_id')
645
654
646 return extras
655 return extras
647
656
648
657
649 def _get_extras_from_commit_id(commit_id, path):
658 def _get_extras_from_commit_id(commit_id, path):
650 extras = {}
659 extras = {}
651 try:
660 try:
652 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
661 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
653 '-r', commit_id,
662 '-r', commit_id,
654 '--revprop', path, 'rc-scm-extras']
663 '--revprop', path, 'rc-scm-extras']
655 stdout, stderr = subprocessio.run_command(
664 stdout, stderr = subprocessio.run_command(
656 cmd, env=os.environ.copy())
665 cmd, env=os.environ.copy())
657 extras = json.loads(base64.urlsafe_b64decode(stdout))
666 extras = json.loads(base64.urlsafe_b64decode(stdout))
658 except Exception:
667 except Exception:
659 log.exception('Failed to extract extras info from commit_id')
668 log.exception('Failed to extract extras info from commit_id')
660
669
661 return extras
670 return extras
662
671
663
672
664 def svn_pre_commit(repo_path, commit_data, env):
673 def svn_pre_commit(repo_path, commit_data, env):
665 path, txn_id = commit_data
674 path, txn_id = commit_data
666 branches = []
675 branches = []
667 tags = []
676 tags = []
668
677
669 if env.get('RC_SCM_DATA'):
678 if env.get('RC_SCM_DATA'):
670 extras = json.loads(env['RC_SCM_DATA'])
679 extras = json.loads(env['RC_SCM_DATA'])
671 else:
680 else:
672 # fallback method to read from TXN-ID stored data
681 # fallback method to read from TXN-ID stored data
673 extras = _get_extras_from_txn_id(path, txn_id)
682 extras = _get_extras_from_txn_id(path, txn_id)
674 if not extras:
683 if not extras:
675 return 0
684 return 0
676
685
677 extras['hook_type'] = 'pre_commit'
686 extras['hook_type'] = 'pre_commit'
678 extras['commit_ids'] = [txn_id]
687 extras['commit_ids'] = [txn_id]
679 extras['txn_id'] = txn_id
688 extras['txn_id'] = txn_id
680 extras['new_refs'] = {
689 extras['new_refs'] = {
681 'total_commits': 1,
690 'total_commits': 1,
682 'branches': branches,
691 'branches': branches,
683 'bookmarks': [],
692 'bookmarks': [],
684 'tags': tags,
693 'tags': tags,
685 }
694 }
686
695
687 return _call_hook('pre_push', extras, SvnMessageWriter())
696 return _call_hook('pre_push', extras, SvnMessageWriter())
688
697
689
698
690 def svn_post_commit(repo_path, commit_data, env):
699 def svn_post_commit(repo_path, commit_data, env):
691 """
700 """
692 commit_data is path, rev, txn_id
701 commit_data is path, rev, txn_id
693 """
702 """
694 if len(commit_data) == 3:
703 if len(commit_data) == 3:
695 path, commit_id, txn_id = commit_data
704 path, commit_id, txn_id = commit_data
696 elif len(commit_data) == 2:
705 elif len(commit_data) == 2:
697 log.error('Failed to extract txn_id from commit_data using legacy method. '
706 log.error('Failed to extract txn_id from commit_data using legacy method. '
698 'Some functionality might be limited')
707 'Some functionality might be limited')
699 path, commit_id = commit_data
708 path, commit_id = commit_data
700 txn_id = None
709 txn_id = None
701
710
702 branches = []
711 branches = []
703 tags = []
712 tags = []
704
713
705 if env.get('RC_SCM_DATA'):
714 if env.get('RC_SCM_DATA'):
706 extras = json.loads(env['RC_SCM_DATA'])
715 extras = json.loads(env['RC_SCM_DATA'])
707 else:
716 else:
708 # fallback method to read from TXN-ID stored data
717 # fallback method to read from TXN-ID stored data
709 extras = _get_extras_from_commit_id(commit_id, path)
718 extras = _get_extras_from_commit_id(commit_id, path)
710 if not extras:
719 if not extras:
711 return 0
720 return 0
712
721
713 extras['hook_type'] = 'post_commit'
722 extras['hook_type'] = 'post_commit'
714 extras['commit_ids'] = [commit_id]
723 extras['commit_ids'] = [commit_id]
715 extras['txn_id'] = txn_id
724 extras['txn_id'] = txn_id
716 extras['new_refs'] = {
725 extras['new_refs'] = {
717 'branches': branches,
726 'branches': branches,
718 'bookmarks': [],
727 'bookmarks': [],
719 'tags': tags,
728 'tags': tags,
720 'total_commits': 1,
729 'total_commits': 1,
721 }
730 }
722
731
723 if 'repo_size' in extras['hooks']:
732 if 'repo_size' in extras['hooks']:
724 try:
733 try:
725 _call_hook('repo_size', extras, SvnMessageWriter())
734 _call_hook('repo_size', extras, SvnMessageWriter())
726 except Exception:
735 except Exception:
727 pass
736 pass
728
737
729 return _call_hook('post_push', extras, SvnMessageWriter())
738 return _call_hook('post_push', extras, SvnMessageWriter())
@@ -1,739 +1,740 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import io
18 import os
19 import os
19 import sys
20 import sys
20 import base64
21 import base64
21 import locale
22 import locale
22 import logging
23 import logging
23 import uuid
24 import uuid
24 import time
25 import time
25 import wsgiref.util
26 import wsgiref.util
26 import traceback
27 import traceback
27 import tempfile
28 import tempfile
28 import psutil
29 import psutil
29
30
30 from itertools import chain
31 from itertools import chain
31 from io import StringIO
32
32
33 import simplejson as json
34 import msgpack
33 import msgpack
35 import configparser
34 import configparser
36
35
37 from pyramid.config import Configurator
36 from pyramid.config import Configurator
38 from pyramid.wsgi import wsgiapp
37 from pyramid.wsgi import wsgiapp
39 from pyramid.response import Response
38 from pyramid.response import Response
40
39
40 from vcsserver.lib.rc_json import json
41 from vcsserver.config.settings_maker import SettingsMaker
41 from vcsserver.config.settings_maker import SettingsMaker
42 from vcsserver.utils import safe_int
42 from vcsserver.utils import safe_int
43 from vcsserver.lib.statsd_client import StatsdClient
43 from vcsserver.lib.statsd_client import StatsdClient
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
47 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
48 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
48 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
49
49
50 try:
50 try:
51 locale.setlocale(locale.LC_ALL, '')
51 locale.setlocale(locale.LC_ALL, '')
52 except locale.Error as e:
52 except locale.Error as e:
53 log.error(
53 log.error(
54 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
54 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
55 os.environ['LC_ALL'] = 'C'
55 os.environ['LC_ALL'] = 'C'
56
56
57
57
58 import vcsserver
58 import vcsserver
59 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
59 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
60 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
60 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
61 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
61 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
62 from vcsserver.echo_stub.echo_app import EchoApp
62 from vcsserver.echo_stub.echo_app import EchoApp
63 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
63 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
64 from vcsserver.lib.exc_tracking import store_exception
64 from vcsserver.lib.exc_tracking import store_exception
65 from vcsserver.server import VcsServer
65 from vcsserver.server import VcsServer
66
66
67 strict_vcs = True
67 strict_vcs = True
68
68
69 git_import_err = None
69 git_import_err = None
70 try:
70 try:
71 from vcsserver.remote.git import GitFactory, GitRemote
71 from vcsserver.remote.git import GitFactory, GitRemote
72 except ImportError as e:
72 except ImportError as e:
73 GitFactory = None
73 GitFactory = None
74 GitRemote = None
74 GitRemote = None
75 git_import_err = e
75 git_import_err = e
76 if strict_vcs:
76 if strict_vcs:
77 raise
77 raise
78
78
79
79
80 hg_import_err = None
80 hg_import_err = None
81 try:
81 try:
82 from vcsserver.remote.hg import MercurialFactory, HgRemote
82 from vcsserver.remote.hg import MercurialFactory, HgRemote
83 except ImportError as e:
83 except ImportError as e:
84 MercurialFactory = None
84 MercurialFactory = None
85 HgRemote = None
85 HgRemote = None
86 hg_import_err = e
86 hg_import_err = e
87 if strict_vcs:
87 if strict_vcs:
88 raise
88 raise
89
89
90
90
91 svn_import_err = None
91 svn_import_err = None
92 try:
92 try:
93 from vcsserver.remote.svn import SubversionFactory, SvnRemote
93 from vcsserver.remote.svn import SubversionFactory, SvnRemote
94 except ImportError as e:
94 except ImportError as e:
95 SubversionFactory = None
95 SubversionFactory = None
96 SvnRemote = None
96 SvnRemote = None
97 svn_import_err = e
97 svn_import_err = e
98 if strict_vcs:
98 if strict_vcs:
99 raise
99 raise
100
100
101
101
102 def _is_request_chunked(environ):
102 def _is_request_chunked(environ):
103 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
103 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
104 return stream
104 return stream
105
105
106
106
107 def log_max_fd():
107 def log_max_fd():
108 try:
108 try:
109 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
109 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
110 log.info('Max file descriptors value: %s', maxfd)
110 log.info('Max file descriptors value: %s', maxfd)
111 except Exception:
111 except Exception:
112 pass
112 pass
113
113
114
114
115 class VCS(object):
115 class VCS(object):
116 def __init__(self, locale_conf=None, cache_config=None):
116 def __init__(self, locale_conf=None, cache_config=None):
117 self.locale = locale_conf
117 self.locale = locale_conf
118 self.cache_config = cache_config
118 self.cache_config = cache_config
119 self._configure_locale()
119 self._configure_locale()
120
120
121 log_max_fd()
121 log_max_fd()
122
122
123 if GitFactory and GitRemote:
123 if GitFactory and GitRemote:
124 git_factory = GitFactory()
124 git_factory = GitFactory()
125 self._git_remote = GitRemote(git_factory)
125 self._git_remote = GitRemote(git_factory)
126 else:
126 else:
127 log.error("Git client import failed: %s", git_import_err)
127 log.error("Git client import failed: %s", git_import_err)
128
128
129 if MercurialFactory and HgRemote:
129 if MercurialFactory and HgRemote:
130 hg_factory = MercurialFactory()
130 hg_factory = MercurialFactory()
131 self._hg_remote = HgRemote(hg_factory)
131 self._hg_remote = HgRemote(hg_factory)
132 else:
132 else:
133 log.error("Mercurial client import failed: %s", hg_import_err)
133 log.error("Mercurial client import failed: %s", hg_import_err)
134
134
135 if SubversionFactory and SvnRemote:
135 if SubversionFactory and SvnRemote:
136 svn_factory = SubversionFactory()
136 svn_factory = SubversionFactory()
137
137
138 # hg factory is used for svn url validation
138 # hg factory is used for svn url validation
139 hg_factory = MercurialFactory()
139 hg_factory = MercurialFactory()
140 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
140 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
141 else:
141 else:
142 log.error("Subversion client import failed: %s", svn_import_err)
142 log.error("Subversion client import failed: %s", svn_import_err)
143
143
144 self._vcsserver = VcsServer()
144 self._vcsserver = VcsServer()
145
145
146 def _configure_locale(self):
146 def _configure_locale(self):
147 if self.locale:
147 if self.locale:
148 log.info('Settings locale: `LC_ALL` to %s', self.locale)
148 log.info('Settings locale: `LC_ALL` to %s', self.locale)
149 else:
149 else:
150 log.info('Configuring locale subsystem based on environment variables')
150 log.info('Configuring locale subsystem based on environment variables')
151 try:
151 try:
152 # If self.locale is the empty string, then the locale
152 # If self.locale is the empty string, then the locale
153 # module will use the environment variables. See the
153 # module will use the environment variables. See the
154 # documentation of the package `locale`.
154 # documentation of the package `locale`.
155 locale.setlocale(locale.LC_ALL, self.locale)
155 locale.setlocale(locale.LC_ALL, self.locale)
156
156
157 language_code, encoding = locale.getlocale()
157 language_code, encoding = locale.getlocale()
158 log.info(
158 log.info(
159 'Locale set to language code "%s" with encoding "%s".',
159 'Locale set to language code "%s" with encoding "%s".',
160 language_code, encoding)
160 language_code, encoding)
161 except locale.Error:
161 except locale.Error:
162 log.exception('Cannot set locale, not configuring the locale system')
162 log.exception('Cannot set locale, not configuring the locale system')
163
163
164
164
165 class WsgiProxy(object):
165 class WsgiProxy(object):
166 def __init__(self, wsgi):
166 def __init__(self, wsgi):
167 self.wsgi = wsgi
167 self.wsgi = wsgi
168
168
169 def __call__(self, environ, start_response):
169 def __call__(self, environ, start_response):
170 input_data = environ['wsgi.input'].read()
170 input_data = environ['wsgi.input'].read()
171 input_data = msgpack.unpackb(input_data)
171 input_data = msgpack.unpackb(input_data)
172
172
173 error = None
173 error = None
174 try:
174 try:
175 data, status, headers = self.wsgi.handle(
175 data, status, headers = self.wsgi.handle(
176 input_data['environment'], input_data['input_data'],
176 input_data['environment'], input_data['input_data'],
177 *input_data['args'], **input_data['kwargs'])
177 *input_data['args'], **input_data['kwargs'])
178 except Exception as e:
178 except Exception as e:
179 data, status, headers = [], None, None
179 data, status, headers = [], None, None
180 error = {
180 error = {
181 'message': str(e),
181 'message': str(e),
182 '_vcs_kind': getattr(e, '_vcs_kind', None)
182 '_vcs_kind': getattr(e, '_vcs_kind', None)
183 }
183 }
184
184
185 start_response(200, {})
185 start_response(200, {})
186 return self._iterator(error, status, headers, data)
186 return self._iterator(error, status, headers, data)
187
187
188 def _iterator(self, error, status, headers, data):
188 def _iterator(self, error, status, headers, data):
189 initial_data = [
189 initial_data = [
190 error,
190 error,
191 status,
191 status,
192 headers,
192 headers,
193 ]
193 ]
194
194
195 for d in chain(initial_data, data):
195 for d in chain(initial_data, data):
196 yield msgpack.packb(d)
196 yield msgpack.packb(d)
197
197
198
198
199 def not_found(request):
199 def not_found(request):
200 return {'status': '404 NOT FOUND'}
200 return {'status': '404 NOT FOUND'}
201
201
202
202
203 class VCSViewPredicate(object):
203 class VCSViewPredicate(object):
204 def __init__(self, val, config):
204 def __init__(self, val, config):
205 self.remotes = val
205 self.remotes = val
206
206
207 def text(self):
207 def text(self):
208 return 'vcs view method = %s' % (list(self.remotes.keys()),)
208 return 'vcs view method = %s' % (list(self.remotes.keys()),)
209
209
210 phash = text
210 phash = text
211
211
212 def __call__(self, context, request):
212 def __call__(self, context, request):
213 """
213 """
214 View predicate that returns true if given backend is supported by
214 View predicate that returns true if given backend is supported by
215 defined remotes.
215 defined remotes.
216 """
216 """
217 backend = request.matchdict.get('backend')
217 backend = request.matchdict.get('backend')
218 return backend in self.remotes
218 return backend in self.remotes
219
219
220
220
221 class HTTPApplication(object):
221 class HTTPApplication(object):
222 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
222 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
223
223
224 remote_wsgi = remote_wsgi
224 remote_wsgi = remote_wsgi
225 _use_echo_app = False
225 _use_echo_app = False
226
226
227 def __init__(self, settings=None, global_config=None):
227 def __init__(self, settings=None, global_config=None):
228
228
229 self.config = Configurator(settings=settings)
229 self.config = Configurator(settings=settings)
230 # Init our statsd at very start
230 # Init our statsd at very start
231 self.config.registry.statsd = StatsdClient.statsd
231 self.config.registry.statsd = StatsdClient.statsd
232
232
233 self.global_config = global_config
233 self.global_config = global_config
234 self.config.include('vcsserver.lib.rc_cache')
234 self.config.include('vcsserver.lib.rc_cache')
235
235
236 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
236 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
237 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
237 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
238 self._remotes = {
238 self._remotes = {
239 'hg': vcs._hg_remote,
239 'hg': vcs._hg_remote,
240 'git': vcs._git_remote,
240 'git': vcs._git_remote,
241 'svn': vcs._svn_remote,
241 'svn': vcs._svn_remote,
242 'server': vcs._vcsserver,
242 'server': vcs._vcsserver,
243 }
243 }
244 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
244 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
245 self._use_echo_app = True
245 self._use_echo_app = True
246 log.warning("Using EchoApp for VCS operations.")
246 log.warning("Using EchoApp for VCS operations.")
247 self.remote_wsgi = remote_wsgi_stub
247 self.remote_wsgi = remote_wsgi_stub
248
248
249 self._configure_settings(global_config, settings)
249 self._configure_settings(global_config, settings)
250
250
251 self._configure()
251 self._configure()
252
252
253 def _configure_settings(self, global_config, app_settings):
253 def _configure_settings(self, global_config, app_settings):
254 """
254 """
255 Configure the settings module.
255 Configure the settings module.
256 """
256 """
257 settings_merged = global_config.copy()
257 settings_merged = global_config.copy()
258 settings_merged.update(app_settings)
258 settings_merged.update(app_settings)
259
259
260 git_path = app_settings.get('git_path', None)
260 git_path = app_settings.get('git_path', None)
261 if git_path:
261 if git_path:
262 settings.GIT_EXECUTABLE = git_path
262 settings.GIT_EXECUTABLE = git_path
263 binary_dir = app_settings.get('core.binary_dir', None)
263 binary_dir = app_settings.get('core.binary_dir', None)
264 if binary_dir:
264 if binary_dir:
265 settings.BINARY_DIR = binary_dir
265 settings.BINARY_DIR = binary_dir
266
266
267 # Store the settings to make them available to other modules.
267 # Store the settings to make them available to other modules.
268 vcsserver.PYRAMID_SETTINGS = settings_merged
268 vcsserver.PYRAMID_SETTINGS = settings_merged
269 vcsserver.CONFIG = settings_merged
269 vcsserver.CONFIG = settings_merged
270
270
271 def _configure(self):
271 def _configure(self):
272 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
272 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
273
273
274 self.config.add_route('service', '/_service')
274 self.config.add_route('service', '/_service')
275 self.config.add_route('status', '/status')
275 self.config.add_route('status', '/status')
276 self.config.add_route('hg_proxy', '/proxy/hg')
276 self.config.add_route('hg_proxy', '/proxy/hg')
277 self.config.add_route('git_proxy', '/proxy/git')
277 self.config.add_route('git_proxy', '/proxy/git')
278
278
279 # rpc methods
279 # rpc methods
280 self.config.add_route('vcs', '/{backend}')
280 self.config.add_route('vcs', '/{backend}')
281
281
282 # streaming rpc remote methods
282 # streaming rpc remote methods
283 self.config.add_route('vcs_stream', '/{backend}/stream')
283 self.config.add_route('vcs_stream', '/{backend}/stream')
284
284
285 # vcs operations clone/push as streaming
285 # vcs operations clone/push as streaming
286 self.config.add_route('stream_git', '/stream/git/*repo_name')
286 self.config.add_route('stream_git', '/stream/git/*repo_name')
287 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
287 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
288
288
289 self.config.add_view(self.status_view, route_name='status', renderer='json')
289 self.config.add_view(self.status_view, route_name='status', renderer='json')
290 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
290 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
291
291
292 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
292 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
293 self.config.add_view(self.git_proxy(), route_name='git_proxy')
293 self.config.add_view(self.git_proxy(), route_name='git_proxy')
294 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
294 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
295 vcs_view=self._remotes)
295 vcs_view=self._remotes)
296 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
296 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
297 vcs_view=self._remotes)
297 vcs_view=self._remotes)
298
298
299 self.config.add_view(self.hg_stream(), route_name='stream_hg')
299 self.config.add_view(self.hg_stream(), route_name='stream_hg')
300 self.config.add_view(self.git_stream(), route_name='stream_git')
300 self.config.add_view(self.git_stream(), route_name='stream_git')
301
301
302 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
302 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
303
303
304 self.config.add_notfound_view(not_found, renderer='json')
304 self.config.add_notfound_view(not_found, renderer='json')
305
305
306 self.config.add_view(self.handle_vcs_exception, context=Exception)
306 self.config.add_view(self.handle_vcs_exception, context=Exception)
307
307
308 self.config.add_tween(
308 self.config.add_tween(
309 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
309 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
310 )
310 )
311 self.config.add_request_method(
311 self.config.add_request_method(
312 'vcsserver.lib.request_counter.get_request_counter',
312 'vcsserver.lib.request_counter.get_request_counter',
313 'request_count')
313 'request_count')
314
314
315 def wsgi_app(self):
315 def wsgi_app(self):
316 return self.config.make_wsgi_app()
316 return self.config.make_wsgi_app()
317
317
318 def _vcs_view_params(self, request):
318 def _vcs_view_params(self, request):
319 remote = self._remotes[request.matchdict['backend']]
319 remote = self._remotes[request.matchdict['backend']]
320 payload = msgpack.unpackb(request.body, use_list=True)
320 payload = msgpack.unpackb(request.body, use_list=True, raw=False)
321
321 method = payload.get('method')
322 method = payload.get('method')
322 params = payload['params']
323 params = payload['params']
323 wire = params.get('wire')
324 wire = params.get('wire')
324 args = params.get('args')
325 args = params.get('args')
325 kwargs = params.get('kwargs')
326 kwargs = params.get('kwargs')
326 context_uid = None
327 context_uid = None
327
328
328 if wire:
329 if wire:
329 try:
330 try:
330 wire['context'] = context_uid = uuid.UUID(wire['context'])
331 wire['context'] = context_uid = uuid.UUID(wire['context'])
331 except KeyError:
332 except KeyError:
332 pass
333 pass
333 args.insert(0, wire)
334 args.insert(0, wire)
334 repo_state_uid = wire.get('repo_state_uid') if wire else None
335 repo_state_uid = wire.get('repo_state_uid') if wire else None
335
336
336 # NOTE(marcink): trading complexity for slight performance
337 # NOTE(marcink): trading complexity for slight performance
337 if log.isEnabledFor(logging.DEBUG):
338 if log.isEnabledFor(logging.DEBUG):
338 no_args_methods = [
339 no_args_methods = [
339
340
340 ]
341 ]
341 if method in no_args_methods:
342 if method in no_args_methods:
342 call_args = ''
343 call_args = ''
343 else:
344 else:
344 call_args = args[1:]
345 call_args = args[1:]
345
346
346 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
347 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
347 method, call_args, kwargs, context_uid, repo_state_uid)
348 method, call_args, kwargs, context_uid, repo_state_uid)
348
349
349 statsd = request.registry.statsd
350 statsd = request.registry.statsd
350 if statsd:
351 if statsd:
351 statsd.incr(
352 statsd.incr(
352 'vcsserver_method_total', tags=[
353 'vcsserver_method_total', tags=[
353 "method:{}".format(method),
354 "method:{}".format(method),
354 ])
355 ])
355 return payload, remote, method, args, kwargs
356 return payload, remote, method, args, kwargs
356
357
357 def vcs_view(self, request):
358 def vcs_view(self, request):
358
359
359 payload, remote, method, args, kwargs = self._vcs_view_params(request)
360 payload, remote, method, args, kwargs = self._vcs_view_params(request)
360 payload_id = payload.get('id')
361 payload_id = payload.get('id')
361
362
362 try:
363 try:
363 resp = getattr(remote, method)(*args, **kwargs)
364 resp = getattr(remote, method)(*args, **kwargs)
364 except Exception as e:
365 except Exception as e:
365 exc_info = list(sys.exc_info())
366 exc_info = list(sys.exc_info())
366 exc_type, exc_value, exc_traceback = exc_info
367 exc_type, exc_value, exc_traceback = exc_info
367
368
368 org_exc = getattr(e, '_org_exc', None)
369 org_exc = getattr(e, '_org_exc', None)
369 org_exc_name = None
370 org_exc_name = None
370 org_exc_tb = ''
371 org_exc_tb = ''
371 if org_exc:
372 if org_exc:
372 org_exc_name = org_exc.__class__.__name__
373 org_exc_name = org_exc.__class__.__name__
373 org_exc_tb = getattr(e, '_org_exc_tb', '')
374 org_exc_tb = getattr(e, '_org_exc_tb', '')
374 # replace our "faked" exception with our org
375 # replace our "faked" exception with our org
375 exc_info[0] = org_exc.__class__
376 exc_info[0] = org_exc.__class__
376 exc_info[1] = org_exc
377 exc_info[1] = org_exc
377
378
378 should_store_exc = True
379 should_store_exc = True
379 if org_exc:
380 if org_exc:
380 def get_exc_fqn(_exc_obj):
381 def get_exc_fqn(_exc_obj):
381 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
382 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
382 return module_name + '.' + org_exc_name
383 return module_name + '.' + org_exc_name
383
384
384 exc_fqn = get_exc_fqn(org_exc)
385 exc_fqn = get_exc_fqn(org_exc)
385
386
386 if exc_fqn in ['mercurial.error.RepoLookupError',
387 if exc_fqn in ['mercurial.error.RepoLookupError',
387 'vcsserver.exceptions.RefNotFoundException']:
388 'vcsserver.exceptions.RefNotFoundException']:
388 should_store_exc = False
389 should_store_exc = False
389
390
390 if should_store_exc:
391 if should_store_exc:
391 store_exception(id(exc_info), exc_info, request_path=request.path)
392 store_exception(id(exc_info), exc_info, request_path=request.path)
392
393
393 tb_info = ''.join(
394 tb_info = ''.join(
394 traceback.format_exception(exc_type, exc_value, exc_traceback))
395 traceback.format_exception(exc_type, exc_value, exc_traceback))
395
396
396 type_ = e.__class__.__name__
397 type_ = e.__class__.__name__
397 if type_ not in self.ALLOWED_EXCEPTIONS:
398 if type_ not in self.ALLOWED_EXCEPTIONS:
398 type_ = None
399 type_ = None
399
400
400 resp = {
401 resp = {
401 'id': payload_id,
402 'id': payload_id,
402 'error': {
403 'error': {
403 'message': e.message,
404 'message': str(e),
404 'traceback': tb_info,
405 'traceback': tb_info,
405 'org_exc': org_exc_name,
406 'org_exc': org_exc_name,
406 'org_exc_tb': org_exc_tb,
407 'org_exc_tb': org_exc_tb,
407 'type': type_
408 'type': type_
408 }
409 }
409 }
410 }
410
411
411 try:
412 try:
412 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
413 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
413 except AttributeError:
414 except AttributeError:
414 pass
415 pass
415 else:
416 else:
416 resp = {
417 resp = {
417 'id': payload_id,
418 'id': payload_id,
418 'result': resp
419 'result': resp
419 }
420 }
420
421
421 return resp
422 return resp
422
423
423 def vcs_stream_view(self, request):
424 def vcs_stream_view(self, request):
424 payload, remote, method, args, kwargs = self._vcs_view_params(request)
425 payload, remote, method, args, kwargs = self._vcs_view_params(request)
425 # this method has a stream: marker we remove it here
426 # this method has a stream: marker we remove it here
426 method = method.split('stream:')[-1]
427 method = method.split('stream:')[-1]
427 chunk_size = safe_int(payload.get('chunk_size')) or 4096
428 chunk_size = safe_int(payload.get('chunk_size')) or 4096
428
429
429 try:
430 try:
430 resp = getattr(remote, method)(*args, **kwargs)
431 resp = getattr(remote, method)(*args, **kwargs)
431 except Exception as e:
432 except Exception as e:
432 raise
433 raise
433
434
434 def get_chunked_data(method_resp):
435 def get_chunked_data(method_resp):
435 stream = StringIO(method_resp)
436 stream = io.BytesIO(method_resp)
436 while 1:
437 while 1:
437 chunk = stream.read(chunk_size)
438 chunk = stream.read(chunk_size)
438 if not chunk:
439 if not chunk:
439 break
440 break
440 yield chunk
441 yield chunk
441
442
442 response = Response(app_iter=get_chunked_data(resp))
443 response = Response(app_iter=get_chunked_data(resp))
443 response.content_type = 'application/octet-stream'
444 response.content_type = 'application/octet-stream'
444
445
445 return response
446 return response
446
447
447 def status_view(self, request):
448 def status_view(self, request):
448 import vcsserver
449 import vcsserver
449 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
450 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
450 'pid': os.getpid()}
451 'pid': os.getpid()}
451
452
452 def service_view(self, request):
453 def service_view(self, request):
453 import vcsserver
454 import vcsserver
454
455
455 payload = msgpack.unpackb(request.body, use_list=True)
456 payload = msgpack.unpackb(request.body, use_list=True)
456 server_config, app_config = {}, {}
457 server_config, app_config = {}, {}
457
458
458 try:
459 try:
459 path = self.global_config['__file__']
460 path = self.global_config['__file__']
460 config = configparser.RawConfigParser()
461 config = configparser.RawConfigParser()
461
462
462 config.read(path)
463 config.read(path)
463
464
464 if config.has_section('server:main'):
465 if config.has_section('server:main'):
465 server_config = dict(config.items('server:main'))
466 server_config = dict(config.items('server:main'))
466 if config.has_section('app:main'):
467 if config.has_section('app:main'):
467 app_config = dict(config.items('app:main'))
468 app_config = dict(config.items('app:main'))
468
469
469 except Exception:
470 except Exception:
470 log.exception('Failed to read .ini file for display')
471 log.exception('Failed to read .ini file for display')
471
472
472 environ = list(os.environ.items())
473 environ = list(os.environ.items())
473
474
474 resp = {
475 resp = {
475 'id': payload.get('id'),
476 'id': payload.get('id'),
476 'result': dict(
477 'result': dict(
477 version=vcsserver.__version__,
478 version=vcsserver.__version__,
478 config=server_config,
479 config=server_config,
479 app_config=app_config,
480 app_config=app_config,
480 environ=environ,
481 environ=environ,
481 payload=payload,
482 payload=payload,
482 )
483 )
483 }
484 }
484 return resp
485 return resp
485
486
486 def _msgpack_renderer_factory(self, info):
487 def _msgpack_renderer_factory(self, info):
487 def _render(value, system):
488 def _render(value, system):
488 request = system.get('request')
489 request = system.get('request')
489 if request is not None:
490 if request is not None:
490 response = request.response
491 response = request.response
491 ct = response.content_type
492 ct = response.content_type
492 if ct == response.default_content_type:
493 if ct == response.default_content_type:
493 response.content_type = 'application/x-msgpack'
494 response.content_type = 'application/x-msgpack'
494 return msgpack.packb(value)
495 return msgpack.packb(value)
495 return _render
496 return _render
496
497
497 def set_env_from_config(self, environ, config):
498 def set_env_from_config(self, environ, config):
498 dict_conf = {}
499 dict_conf = {}
499 try:
500 try:
500 for elem in config:
501 for elem in config:
501 if elem[0] == 'rhodecode':
502 if elem[0] == 'rhodecode':
502 dict_conf = json.loads(elem[2])
503 dict_conf = json.loads(elem[2])
503 break
504 break
504 except Exception:
505 except Exception:
505 log.exception('Failed to fetch SCM CONFIG')
506 log.exception('Failed to fetch SCM CONFIG')
506 return
507 return
507
508
508 username = dict_conf.get('username')
509 username = dict_conf.get('username')
509 if username:
510 if username:
510 environ['REMOTE_USER'] = username
511 environ['REMOTE_USER'] = username
511 # mercurial specific, some extension api rely on this
512 # mercurial specific, some extension api rely on this
512 environ['HGUSER'] = username
513 environ['HGUSER'] = username
513
514
514 ip = dict_conf.get('ip')
515 ip = dict_conf.get('ip')
515 if ip:
516 if ip:
516 environ['REMOTE_HOST'] = ip
517 environ['REMOTE_HOST'] = ip
517
518
518 if _is_request_chunked(environ):
519 if _is_request_chunked(environ):
519 # set the compatibility flag for webob
520 # set the compatibility flag for webob
520 environ['wsgi.input_terminated'] = True
521 environ['wsgi.input_terminated'] = True
521
522
522 def hg_proxy(self):
523 def hg_proxy(self):
523 @wsgiapp
524 @wsgiapp
524 def _hg_proxy(environ, start_response):
525 def _hg_proxy(environ, start_response):
525 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
526 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
526 return app(environ, start_response)
527 return app(environ, start_response)
527 return _hg_proxy
528 return _hg_proxy
528
529
529 def git_proxy(self):
530 def git_proxy(self):
530 @wsgiapp
531 @wsgiapp
531 def _git_proxy(environ, start_response):
532 def _git_proxy(environ, start_response):
532 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
533 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
533 return app(environ, start_response)
534 return app(environ, start_response)
534 return _git_proxy
535 return _git_proxy
535
536
536 def hg_stream(self):
537 def hg_stream(self):
537 if self._use_echo_app:
538 if self._use_echo_app:
538 @wsgiapp
539 @wsgiapp
539 def _hg_stream(environ, start_response):
540 def _hg_stream(environ, start_response):
540 app = EchoApp('fake_path', 'fake_name', None)
541 app = EchoApp('fake_path', 'fake_name', None)
541 return app(environ, start_response)
542 return app(environ, start_response)
542 return _hg_stream
543 return _hg_stream
543 else:
544 else:
544 @wsgiapp
545 @wsgiapp
545 def _hg_stream(environ, start_response):
546 def _hg_stream(environ, start_response):
546 log.debug('http-app: handling hg stream')
547 log.debug('http-app: handling hg stream')
547 repo_path = environ['HTTP_X_RC_REPO_PATH']
548 repo_path = environ['HTTP_X_RC_REPO_PATH']
548 repo_name = environ['HTTP_X_RC_REPO_NAME']
549 repo_name = environ['HTTP_X_RC_REPO_NAME']
549 packed_config = base64.b64decode(
550 packed_config = base64.b64decode(
550 environ['HTTP_X_RC_REPO_CONFIG'])
551 environ['HTTP_X_RC_REPO_CONFIG'])
551 config = msgpack.unpackb(packed_config)
552 config = msgpack.unpackb(packed_config)
552 app = scm_app.create_hg_wsgi_app(
553 app = scm_app.create_hg_wsgi_app(
553 repo_path, repo_name, config)
554 repo_path, repo_name, config)
554
555
555 # Consistent path information for hgweb
556 # Consistent path information for hgweb
556 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
557 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
557 environ['REPO_NAME'] = repo_name
558 environ['REPO_NAME'] = repo_name
558 self.set_env_from_config(environ, config)
559 self.set_env_from_config(environ, config)
559
560
560 log.debug('http-app: starting app handler '
561 log.debug('http-app: starting app handler '
561 'with %s and process request', app)
562 'with %s and process request', app)
562 return app(environ, ResponseFilter(start_response))
563 return app(environ, ResponseFilter(start_response))
563 return _hg_stream
564 return _hg_stream
564
565
565 def git_stream(self):
566 def git_stream(self):
566 if self._use_echo_app:
567 if self._use_echo_app:
567 @wsgiapp
568 @wsgiapp
568 def _git_stream(environ, start_response):
569 def _git_stream(environ, start_response):
569 app = EchoApp('fake_path', 'fake_name', None)
570 app = EchoApp('fake_path', 'fake_name', None)
570 return app(environ, start_response)
571 return app(environ, start_response)
571 return _git_stream
572 return _git_stream
572 else:
573 else:
573 @wsgiapp
574 @wsgiapp
574 def _git_stream(environ, start_response):
575 def _git_stream(environ, start_response):
575 log.debug('http-app: handling git stream')
576 log.debug('http-app: handling git stream')
576 repo_path = environ['HTTP_X_RC_REPO_PATH']
577 repo_path = environ['HTTP_X_RC_REPO_PATH']
577 repo_name = environ['HTTP_X_RC_REPO_NAME']
578 repo_name = environ['HTTP_X_RC_REPO_NAME']
578 packed_config = base64.b64decode(
579 packed_config = base64.b64decode(
579 environ['HTTP_X_RC_REPO_CONFIG'])
580 environ['HTTP_X_RC_REPO_CONFIG'])
580 config = msgpack.unpackb(packed_config)
581 config = msgpack.unpackb(packed_config, raw=False)
581
582
582 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
583 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
583 self.set_env_from_config(environ, config)
584 self.set_env_from_config(environ, config)
584
585
585 content_type = environ.get('CONTENT_TYPE', '')
586 content_type = environ.get('CONTENT_TYPE', '')
586
587
587 path = environ['PATH_INFO']
588 path = environ['PATH_INFO']
588 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
589 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
589 log.debug(
590 log.debug(
590 'LFS: Detecting if request `%s` is LFS server path based '
591 'LFS: Detecting if request `%s` is LFS server path based '
591 'on content type:`%s`, is_lfs:%s',
592 'on content type:`%s`, is_lfs:%s',
592 path, content_type, is_lfs_request)
593 path, content_type, is_lfs_request)
593
594
594 if not is_lfs_request:
595 if not is_lfs_request:
595 # fallback detection by path
596 # fallback detection by path
596 if GIT_LFS_PROTO_PAT.match(path):
597 if GIT_LFS_PROTO_PAT.match(path):
597 is_lfs_request = True
598 is_lfs_request = True
598 log.debug(
599 log.debug(
599 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
600 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
600 path, is_lfs_request)
601 path, is_lfs_request)
601
602
602 if is_lfs_request:
603 if is_lfs_request:
603 app = scm_app.create_git_lfs_wsgi_app(
604 app = scm_app.create_git_lfs_wsgi_app(
604 repo_path, repo_name, config)
605 repo_path, repo_name, config)
605 else:
606 else:
606 app = scm_app.create_git_wsgi_app(
607 app = scm_app.create_git_wsgi_app(
607 repo_path, repo_name, config)
608 repo_path, repo_name, config)
608
609
609 log.debug('http-app: starting app handler '
610 log.debug('http-app: starting app handler '
610 'with %s and process request', app)
611 'with %s and process request', app)
611
612
612 return app(environ, start_response)
613 return app(environ, start_response)
613
614
614 return _git_stream
615 return _git_stream
615
616
616 def handle_vcs_exception(self, exception, request):
617 def handle_vcs_exception(self, exception, request):
617 _vcs_kind = getattr(exception, '_vcs_kind', '')
618 _vcs_kind = getattr(exception, '_vcs_kind', '')
618 if _vcs_kind == 'repo_locked':
619 if _vcs_kind == 'repo_locked':
619 # Get custom repo-locked status code if present.
620 # Get custom repo-locked status code if present.
620 status_code = request.headers.get('X-RC-Locked-Status-Code')
621 status_code = request.headers.get('X-RC-Locked-Status-Code')
621 return HTTPRepoLocked(
622 return HTTPRepoLocked(
622 title=exception.message, status_code=status_code)
623 title=exception.message, status_code=status_code)
623
624
624 elif _vcs_kind == 'repo_branch_protected':
625 elif _vcs_kind == 'repo_branch_protected':
625 # Get custom repo-branch-protected status code if present.
626 # Get custom repo-branch-protected status code if present.
626 return HTTPRepoBranchProtected(title=exception.message)
627 return HTTPRepoBranchProtected(title=exception.message)
627
628
628 exc_info = request.exc_info
629 exc_info = request.exc_info
629 store_exception(id(exc_info), exc_info)
630 store_exception(id(exc_info), exc_info)
630
631
631 traceback_info = 'unavailable'
632 traceback_info = 'unavailable'
632 if request.exc_info:
633 if request.exc_info:
633 exc_type, exc_value, exc_tb = request.exc_info
634 exc_type, exc_value, exc_tb = request.exc_info
634 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
635 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
635
636
636 log.error(
637 log.error(
637 'error occurred handling this request for path: %s, \n tb: %s',
638 'error occurred handling this request for path: %s, \n tb: %s',
638 request.path, traceback_info)
639 request.path, traceback_info)
639
640
640 statsd = request.registry.statsd
641 statsd = request.registry.statsd
641 if statsd:
642 if statsd:
642 exc_type = "{}.{}".format(exception.__class__.__module__, exception.__class__.__name__)
643 exc_type = "{}.{}".format(exception.__class__.__module__, exception.__class__.__name__)
643 statsd.incr('vcsserver_exception_total',
644 statsd.incr('vcsserver_exception_total',
644 tags=["type:{}".format(exc_type)])
645 tags=["type:{}".format(exc_type)])
645 raise exception
646 raise exception
646
647
647
648
648 class ResponseFilter(object):
649 class ResponseFilter(object):
649
650
650 def __init__(self, start_response):
651 def __init__(self, start_response):
651 self._start_response = start_response
652 self._start_response = start_response
652
653
653 def __call__(self, status, response_headers, exc_info=None):
654 def __call__(self, status, response_headers, exc_info=None):
654 headers = tuple(
655 headers = tuple(
655 (h, v) for h, v in response_headers
656 (h, v) for h, v in response_headers
656 if not wsgiref.util.is_hop_by_hop(h))
657 if not wsgiref.util.is_hop_by_hop(h))
657 return self._start_response(status, headers, exc_info)
658 return self._start_response(status, headers, exc_info)
658
659
659
660
660 def sanitize_settings_and_apply_defaults(global_config, settings):
661 def sanitize_settings_and_apply_defaults(global_config, settings):
661 global_settings_maker = SettingsMaker(global_config)
662 global_settings_maker = SettingsMaker(global_config)
662 settings_maker = SettingsMaker(settings)
663 settings_maker = SettingsMaker(settings)
663
664
664 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
665 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
665
666
666 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
667 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
667 settings_maker.enable_logging(logging_conf)
668 settings_maker.enable_logging(logging_conf)
668
669
669 # Default includes, possible to change as a user
670 # Default includes, possible to change as a user
670 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
671 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
671 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
672 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
672
673
673 settings_maker.make_setting('__file__', global_config.get('__file__'))
674 settings_maker.make_setting('__file__', global_config.get('__file__'))
674
675
675 settings_maker.make_setting('pyramid.default_locale_name', 'en')
676 settings_maker.make_setting('pyramid.default_locale_name', 'en')
676 settings_maker.make_setting('locale', 'en_US.UTF-8')
677 settings_maker.make_setting('locale', 'en_US.UTF-8')
677
678
678 settings_maker.make_setting('core.binary_dir', '')
679 settings_maker.make_setting('core.binary_dir', '')
679
680
680 temp_store = tempfile.gettempdir()
681 temp_store = tempfile.gettempdir()
681 default_cache_dir = os.path.join(temp_store, 'rc_cache')
682 default_cache_dir = os.path.join(temp_store, 'rc_cache')
682 # save default, cache dir, and use it for all backends later.
683 # save default, cache dir, and use it for all backends later.
683 default_cache_dir = settings_maker.make_setting(
684 default_cache_dir = settings_maker.make_setting(
684 'cache_dir',
685 'cache_dir',
685 default=default_cache_dir, default_when_empty=True,
686 default=default_cache_dir, default_when_empty=True,
686 parser='dir:ensured')
687 parser='dir:ensured')
687
688
688 # exception store cache
689 # exception store cache
689 settings_maker.make_setting(
690 settings_maker.make_setting(
690 'exception_tracker.store_path',
691 'exception_tracker.store_path',
691 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
692 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
692 parser='dir:ensured'
693 parser='dir:ensured'
693 )
694 )
694
695
695 # repo_object cache defaults
696 # repo_object cache defaults
696 settings_maker.make_setting(
697 settings_maker.make_setting(
697 'rc_cache.repo_object.backend',
698 'rc_cache.repo_object.backend',
698 default='dogpile.cache.rc.file_namespace',
699 default='dogpile.cache.rc.file_namespace',
699 parser='string')
700 parser='string')
700 settings_maker.make_setting(
701 settings_maker.make_setting(
701 'rc_cache.repo_object.expiration_time',
702 'rc_cache.repo_object.expiration_time',
702 default=30 * 24 * 60 * 60, # 30days
703 default=30 * 24 * 60 * 60, # 30days
703 parser='int')
704 parser='int')
704 settings_maker.make_setting(
705 settings_maker.make_setting(
705 'rc_cache.repo_object.arguments.filename',
706 'rc_cache.repo_object.arguments.filename',
706 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
707 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
707 parser='string')
708 parser='string')
708
709
709 # statsd
710 # statsd
710 settings_maker.make_setting('statsd.enabled', False, parser='bool')
711 settings_maker.make_setting('statsd.enabled', False, parser='bool')
711 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
712 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
712 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
713 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
713 settings_maker.make_setting('statsd.statsd_prefix', '')
714 settings_maker.make_setting('statsd.statsd_prefix', '')
714 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
715 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
715
716
716 settings_maker.env_expand()
717 settings_maker.env_expand()
717
718
718
719
719 def main(global_config, **settings):
720 def main(global_config, **settings):
720 start_time = time.time()
721 start_time = time.time()
721 log.info('Pyramid app config starting')
722 log.info('Pyramid app config starting')
722
723
723 if MercurialFactory:
724 if MercurialFactory:
724 hgpatches.patch_largefiles_capabilities()
725 hgpatches.patch_largefiles_capabilities()
725 hgpatches.patch_subrepo_type_mapping()
726 hgpatches.patch_subrepo_type_mapping()
726
727
727 # Fill in and sanitize the defaults & do ENV expansion
728 # Fill in and sanitize the defaults & do ENV expansion
728 sanitize_settings_and_apply_defaults(global_config, settings)
729 sanitize_settings_and_apply_defaults(global_config, settings)
729
730
730 # init and bootstrap StatsdClient
731 # init and bootstrap StatsdClient
731 StatsdClient.setup(settings)
732 StatsdClient.setup(settings)
732
733
733 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
734 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
734 total_time = time.time() - start_time
735 total_time = time.time() - start_time
735 log.info('Pyramid app `%s` created and configured in %.2fs',
736 log.info('Pyramid app `%s` created and configured in %.2fs',
736 getattr(pyramid_app, 'func_name', 'pyramid_app'), total_time)
737 getattr(pyramid_app, 'func_name', 'pyramid_app'), total_time)
737 return pyramid_app
738 return pyramid_app
738
739
739
740
@@ -1,329 +1,330 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import time
18 import time
19 import errno
19 import errno
20 import logging
20 import logging
21
21
22 import msgpack
22 import msgpack
23 import redis
23 import redis
24 import pickle
24
25
25 from dogpile.cache.api import CachedValue
26 from dogpile.cache.api import CachedValue
26 from dogpile.cache.backends import memory as memory_backend
27 from dogpile.cache.backends import memory as memory_backend
27 from dogpile.cache.backends import file as file_backend
28 from dogpile.cache.backends import file as file_backend
28 from dogpile.cache.backends import redis as redis_backend
29 from dogpile.cache.backends import redis as redis_backend
29 from dogpile.cache.backends.file import NO_VALUE, FileLock
30 from dogpile.cache.backends.file import NO_VALUE, FileLock
30 from dogpile.cache.util import memoized_property
31 from dogpile.cache.util import memoized_property
31
32
32 from pyramid.settings import asbool
33 from pyramid.settings import asbool
33
34
34 from vcsserver.lib.memory_lru_dict import LRUDict, LRUDictDebug
35 from vcsserver.lib.memory_lru_dict import LRUDict, LRUDictDebug
35 from vcsserver.utils import safe_str, safe_unicode
36 from vcsserver.utils import safe_str
36
37
37
38
38 _default_max_size = 1024
39 _default_max_size = 1024
39
40
40 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
41
42
42
43
43 class LRUMemoryBackend(memory_backend.MemoryBackend):
44 class LRUMemoryBackend(memory_backend.MemoryBackend):
44 key_prefix = 'lru_mem_backend'
45 key_prefix = 'lru_mem_backend'
45 pickle_values = False
46 pickle_values = False
46
47
47 def __init__(self, arguments):
48 def __init__(self, arguments):
48 max_size = arguments.pop('max_size', _default_max_size)
49 max_size = arguments.pop('max_size', _default_max_size)
49
50
50 LRUDictClass = LRUDict
51 LRUDictClass = LRUDict
51 if arguments.pop('log_key_count', None):
52 if arguments.pop('log_key_count', None):
52 LRUDictClass = LRUDictDebug
53 LRUDictClass = LRUDictDebug
53
54
54 arguments['cache_dict'] = LRUDictClass(max_size)
55 arguments['cache_dict'] = LRUDictClass(max_size)
55 super(LRUMemoryBackend, self).__init__(arguments)
56 super(LRUMemoryBackend, self).__init__(arguments)
56
57
57 def delete(self, key):
58 def delete(self, key):
58 try:
59 try:
59 del self._cache[key]
60 del self._cache[key]
60 except KeyError:
61 except KeyError:
61 # we don't care if key isn't there at deletion
62 # we don't care if key isn't there at deletion
62 pass
63 pass
63
64
64 def delete_multi(self, keys):
65 def delete_multi(self, keys):
65 for key in keys:
66 for key in keys:
66 self.delete(key)
67 self.delete(key)
67
68
68
69
69 class PickleSerializer(object):
70 class PickleSerializer(object):
70
71
71 def _dumps(self, value, safe=False):
72 def _dumps(self, value, safe=False):
72 try:
73 try:
73 return pickle.dumps(value)
74 return pickle.dumps(value)
74 except Exception:
75 except Exception:
75 if safe:
76 if safe:
76 return NO_VALUE
77 return NO_VALUE
77 else:
78 else:
78 raise
79 raise
79
80
80 def _loads(self, value, safe=True):
81 def _loads(self, value, safe=True):
81 try:
82 try:
82 return pickle.loads(value)
83 return pickle.loads(value)
83 except Exception:
84 except Exception:
84 if safe:
85 if safe:
85 return NO_VALUE
86 return NO_VALUE
86 else:
87 else:
87 raise
88 raise
88
89
89
90
90 class MsgPackSerializer(object):
91 class MsgPackSerializer(object):
91
92
92 def _dumps(self, value, safe=False):
93 def _dumps(self, value, safe=False):
93 try:
94 try:
94 return msgpack.packb(value)
95 return msgpack.packb(value)
95 except Exception:
96 except Exception:
96 if safe:
97 if safe:
97 return NO_VALUE
98 return NO_VALUE
98 else:
99 else:
99 raise
100 raise
100
101
101 def _loads(self, value, safe=True):
102 def _loads(self, value, safe=True):
102 """
103 """
103 pickle maintained the `CachedValue` wrapper of the tuple
104 pickle maintained the `CachedValue` wrapper of the tuple
104 msgpack does not, so it must be added back in.
105 msgpack does not, so it must be added back in.
105 """
106 """
106 try:
107 try:
107 value = msgpack.unpackb(value, use_list=False)
108 value = msgpack.unpackb(value, use_list=False)
108 return CachedValue(*value)
109 return CachedValue(*value)
109 except Exception:
110 except Exception:
110 if safe:
111 if safe:
111 return NO_VALUE
112 return NO_VALUE
112 else:
113 else:
113 raise
114 raise
114
115
115
116
116 import fcntl
117 import fcntl
117 flock_org = fcntl.flock
118 flock_org = fcntl.flock
118
119
119
120
120 class CustomLockFactory(FileLock):
121 class CustomLockFactory(FileLock):
121
122
122 pass
123 pass
123
124
124
125
125 class FileNamespaceBackend(PickleSerializer, file_backend.DBMBackend):
126 class FileNamespaceBackend(PickleSerializer, file_backend.DBMBackend):
126 key_prefix = 'file_backend'
127 key_prefix = 'file_backend'
127
128
128 def __init__(self, arguments):
129 def __init__(self, arguments):
129 arguments['lock_factory'] = CustomLockFactory
130 arguments['lock_factory'] = CustomLockFactory
130 db_file = arguments.get('filename')
131 db_file = arguments.get('filename')
131
132
132 log.debug('initialing %s DB in %s', self.__class__.__name__, db_file)
133 log.debug('initialing %s DB in %s', self.__class__.__name__, db_file)
133 try:
134 try:
134 super(FileNamespaceBackend, self).__init__(arguments)
135 super(FileNamespaceBackend, self).__init__(arguments)
135 except Exception:
136 except Exception:
136 log.exception('Failed to initialize db at: %s', db_file)
137 log.exception('Failed to initialize db at: %s', db_file)
137 raise
138 raise
138
139
139 def __repr__(self):
140 def __repr__(self):
140 return '{} `{}`'.format(self.__class__, self.filename)
141 return '{} `{}`'.format(self.__class__, self.filename)
141
142
142 def list_keys(self, prefix=''):
143 def list_keys(self, prefix=''):
143 prefix = '{}:{}'.format(self.key_prefix, prefix)
144 prefix = '{}:{}'.format(self.key_prefix, prefix)
144
145
145 def cond(v):
146 def cond(v):
146 if not prefix:
147 if not prefix:
147 return True
148 return True
148
149
149 if v.startswith(prefix):
150 if v.startswith(prefix):
150 return True
151 return True
151 return False
152 return False
152
153
153 with self._dbm_file(True) as dbm:
154 with self._dbm_file(True) as dbm:
154 try:
155 try:
155 return filter(cond, dbm.keys())
156 return filter(cond, dbm.keys())
156 except Exception:
157 except Exception:
157 log.error('Failed to fetch DBM keys from DB: %s', self.get_store())
158 log.error('Failed to fetch DBM keys from DB: %s', self.get_store())
158 raise
159 raise
159
160
160 def get_store(self):
161 def get_store(self):
161 return self.filename
162 return self.filename
162
163
163 def _dbm_get(self, key):
164 def _dbm_get(self, key):
164 with self._dbm_file(False) as dbm:
165 with self._dbm_file(False) as dbm:
165 if hasattr(dbm, 'get'):
166 if hasattr(dbm, 'get'):
166 value = dbm.get(key, NO_VALUE)
167 value = dbm.get(key, NO_VALUE)
167 else:
168 else:
168 # gdbm objects lack a .get method
169 # gdbm objects lack a .get method
169 try:
170 try:
170 value = dbm[key]
171 value = dbm[key]
171 except KeyError:
172 except KeyError:
172 value = NO_VALUE
173 value = NO_VALUE
173 if value is not NO_VALUE:
174 if value is not NO_VALUE:
174 value = self._loads(value)
175 value = self._loads(value)
175 return value
176 return value
176
177
177 def get(self, key):
178 def get(self, key):
178 try:
179 try:
179 return self._dbm_get(key)
180 return self._dbm_get(key)
180 except Exception:
181 except Exception:
181 log.error('Failed to fetch DBM key %s from DB: %s', key, self.get_store())
182 log.error('Failed to fetch DBM key %s from DB: %s', key, self.get_store())
182 raise
183 raise
183
184
184 def set(self, key, value):
185 def set(self, key, value):
185 with self._dbm_file(True) as dbm:
186 with self._dbm_file(True) as dbm:
186 dbm[key] = self._dumps(value)
187 dbm[key] = self._dumps(value)
187
188
188 def set_multi(self, mapping):
189 def set_multi(self, mapping):
189 with self._dbm_file(True) as dbm:
190 with self._dbm_file(True) as dbm:
190 for key, value in mapping.items():
191 for key, value in mapping.items():
191 dbm[key] = self._dumps(value)
192 dbm[key] = self._dumps(value)
192
193
193
194
194 class BaseRedisBackend(redis_backend.RedisBackend):
195 class BaseRedisBackend(redis_backend.RedisBackend):
195 key_prefix = ''
196 key_prefix = ''
196
197
197 def __init__(self, arguments):
198 def __init__(self, arguments):
198 super(BaseRedisBackend, self).__init__(arguments)
199 super(BaseRedisBackend, self).__init__(arguments)
199 self._lock_timeout = self.lock_timeout
200 self._lock_timeout = self.lock_timeout
200 self._lock_auto_renewal = asbool(arguments.pop("lock_auto_renewal", True))
201 self._lock_auto_renewal = asbool(arguments.pop("lock_auto_renewal", True))
201
202
202 if self._lock_auto_renewal and not self._lock_timeout:
203 if self._lock_auto_renewal and not self._lock_timeout:
203 # set default timeout for auto_renewal
204 # set default timeout for auto_renewal
204 self._lock_timeout = 30
205 self._lock_timeout = 30
205
206
206 def _create_client(self):
207 def _create_client(self):
207 args = {}
208 args = {}
208
209
209 if self.url is not None:
210 if self.url is not None:
210 args.update(url=self.url)
211 args.update(url=self.url)
211
212
212 else:
213 else:
213 args.update(
214 args.update(
214 host=self.host, password=self.password,
215 host=self.host, password=self.password,
215 port=self.port, db=self.db
216 port=self.port, db=self.db
216 )
217 )
217
218
218 connection_pool = redis.ConnectionPool(**args)
219 connection_pool = redis.ConnectionPool(**args)
219
220
220 return redis.StrictRedis(connection_pool=connection_pool)
221 return redis.StrictRedis(connection_pool=connection_pool)
221
222
222 def list_keys(self, prefix=''):
223 def list_keys(self, prefix=''):
223 prefix = '{}:{}*'.format(self.key_prefix, prefix)
224 prefix = '{}:{}*'.format(self.key_prefix, prefix)
224 return self.client.keys(prefix)
225 return self.client.keys(prefix)
225
226
226 def get_store(self):
227 def get_store(self):
227 return self.client.connection_pool
228 return self.client.connection_pool
228
229
229 def get(self, key):
230 def get(self, key):
230 value = self.client.get(key)
231 value = self.client.get(key)
231 if value is None:
232 if value is None:
232 return NO_VALUE
233 return NO_VALUE
233 return self._loads(value)
234 return self._loads(value)
234
235
235 def get_multi(self, keys):
236 def get_multi(self, keys):
236 if not keys:
237 if not keys:
237 return []
238 return []
238 values = self.client.mget(keys)
239 values = self.client.mget(keys)
239 loads = self._loads
240 loads = self._loads
240 return [
241 return [
241 loads(v) if v is not None else NO_VALUE
242 loads(v) if v is not None else NO_VALUE
242 for v in values]
243 for v in values]
243
244
244 def set(self, key, value):
245 def set(self, key, value):
245 if self.redis_expiration_time:
246 if self.redis_expiration_time:
246 self.client.setex(key, self.redis_expiration_time,
247 self.client.setex(key, self.redis_expiration_time,
247 self._dumps(value))
248 self._dumps(value))
248 else:
249 else:
249 self.client.set(key, self._dumps(value))
250 self.client.set(key, self._dumps(value))
250
251
251 def set_multi(self, mapping):
252 def set_multi(self, mapping):
252 dumps = self._dumps
253 dumps = self._dumps
253 mapping = dict(
254 mapping = dict(
254 (k, dumps(v))
255 (k, dumps(v))
255 for k, v in mapping.items()
256 for k, v in mapping.items()
256 )
257 )
257
258
258 if not self.redis_expiration_time:
259 if not self.redis_expiration_time:
259 self.client.mset(mapping)
260 self.client.mset(mapping)
260 else:
261 else:
261 pipe = self.client.pipeline()
262 pipe = self.client.pipeline()
262 for key, value in mapping.items():
263 for key, value in mapping.items():
263 pipe.setex(key, self.redis_expiration_time, value)
264 pipe.setex(key, self.redis_expiration_time, value)
264 pipe.execute()
265 pipe.execute()
265
266
266 def get_mutex(self, key):
267 def get_mutex(self, key):
267 if self.distributed_lock:
268 if self.distributed_lock:
268 lock_key = '_lock_{0}'.format(safe_unicode(key))
269 lock_key = '_lock_{0}'.format(safe_str(key))
269 return get_mutex_lock(self.client, lock_key, self._lock_timeout,
270 return get_mutex_lock(self.client, lock_key, self._lock_timeout,
270 auto_renewal=self._lock_auto_renewal)
271 auto_renewal=self._lock_auto_renewal)
271 else:
272 else:
272 return None
273 return None
273
274
274
275
275 class RedisPickleBackend(PickleSerializer, BaseRedisBackend):
276 class RedisPickleBackend(PickleSerializer, BaseRedisBackend):
276 key_prefix = 'redis_pickle_backend'
277 key_prefix = 'redis_pickle_backend'
277 pass
278 pass
278
279
279
280
280 class RedisMsgPackBackend(MsgPackSerializer, BaseRedisBackend):
281 class RedisMsgPackBackend(MsgPackSerializer, BaseRedisBackend):
281 key_prefix = 'redis_msgpack_backend'
282 key_prefix = 'redis_msgpack_backend'
282 pass
283 pass
283
284
284
285
285 def get_mutex_lock(client, lock_key, lock_timeout, auto_renewal=False):
286 def get_mutex_lock(client, lock_key, lock_timeout, auto_renewal=False):
286 import redis_lock
287 import redis_lock
287
288
288 class _RedisLockWrapper(object):
289 class _RedisLockWrapper(object):
289 """LockWrapper for redis_lock"""
290 """LockWrapper for redis_lock"""
290
291
291 @classmethod
292 @classmethod
292 def get_lock(cls):
293 def get_lock(cls):
293 return redis_lock.Lock(
294 return redis_lock.Lock(
294 redis_client=client,
295 redis_client=client,
295 name=lock_key,
296 name=lock_key,
296 expire=lock_timeout,
297 expire=lock_timeout,
297 auto_renewal=auto_renewal,
298 auto_renewal=auto_renewal,
298 strict=True,
299 strict=True,
299 )
300 )
300
301
301 def __repr__(self):
302 def __repr__(self):
302 return "{}:{}".format(self.__class__.__name__, lock_key)
303 return "{}:{}".format(self.__class__.__name__, lock_key)
303
304
304 def __str__(self):
305 def __str__(self):
305 return "{}:{}".format(self.__class__.__name__, lock_key)
306 return "{}:{}".format(self.__class__.__name__, lock_key)
306
307
307 def __init__(self):
308 def __init__(self):
308 self.lock = self.get_lock()
309 self.lock = self.get_lock()
309 self.lock_key = lock_key
310 self.lock_key = lock_key
310
311
311 def acquire(self, wait=True):
312 def acquire(self, wait=True):
312 log.debug('Trying to acquire Redis lock for key %s', self.lock_key)
313 log.debug('Trying to acquire Redis lock for key %s', self.lock_key)
313 try:
314 try:
314 acquired = self.lock.acquire(wait)
315 acquired = self.lock.acquire(wait)
315 log.debug('Got lock for key %s, %s', self.lock_key, acquired)
316 log.debug('Got lock for key %s, %s', self.lock_key, acquired)
316 return acquired
317 return acquired
317 except redis_lock.AlreadyAcquired:
318 except redis_lock.AlreadyAcquired:
318 return False
319 return False
319 except redis_lock.AlreadyStarted:
320 except redis_lock.AlreadyStarted:
320 # refresh thread exists, but it also means we acquired the lock
321 # refresh thread exists, but it also means we acquired the lock
321 return True
322 return True
322
323
323 def release(self):
324 def release(self):
324 try:
325 try:
325 self.lock.release()
326 self.lock.release()
326 except redis_lock.NotAcquired:
327 except redis_lock.NotAcquired:
327 pass
328 pass
328
329
329 return _RedisLockWrapper()
330 return _RedisLockWrapper()
@@ -1,207 +1,207 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import time
19 import time
20 import logging
20 import logging
21 import functools
21 import functools
22 import decorator
22 import decorator
23
23
24 from dogpile.cache import CacheRegion
24 from dogpile.cache import CacheRegion
25
25
26 from vcsserver.utils import safe_str, sha1
26 from vcsserver.utils import safe_bytes, sha1
27 from vcsserver.lib.rc_cache import region_meta
27 from vcsserver.lib.rc_cache import region_meta
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31
31
32 class RhodeCodeCacheRegion(CacheRegion):
32 class RhodeCodeCacheRegion(CacheRegion):
33
33
34 def conditional_cache_on_arguments(
34 def conditional_cache_on_arguments(
35 self, namespace=None,
35 self, namespace=None,
36 expiration_time=None,
36 expiration_time=None,
37 should_cache_fn=None,
37 should_cache_fn=None,
38 to_str=str,
38 to_str=str,
39 function_key_generator=None,
39 function_key_generator=None,
40 condition=True):
40 condition=True):
41 """
41 """
42 Custom conditional decorator, that will not touch any dogpile internals if
42 Custom conditional decorator, that will not touch any dogpile internals if
43 condition isn't meet. This works a bit different than should_cache_fn
43 condition isn't meet. This works a bit different than should_cache_fn
44 And it's faster in cases we don't ever want to compute cached values
44 And it's faster in cases we don't ever want to compute cached values
45 """
45 """
46 expiration_time_is_callable = callable(expiration_time)
46 expiration_time_is_callable = callable(expiration_time)
47
47
48 if function_key_generator is None:
48 if function_key_generator is None:
49 function_key_generator = self.function_key_generator
49 function_key_generator = self.function_key_generator
50
50
51 def get_or_create_for_user_func(key_generator, user_func, *arg, **kw):
51 def get_or_create_for_user_func(key_generator, user_func, *arg, **kw):
52
52
53 if not condition:
53 if not condition:
54 log.debug('Calling un-cached method:%s', user_func.__name__)
54 log.debug('Calling un-cached method:%s', user_func.__name__)
55 start = time.time()
55 start = time.time()
56 result = user_func(*arg, **kw)
56 result = user_func(*arg, **kw)
57 total = time.time() - start
57 total = time.time() - start
58 log.debug('un-cached method:%s took %.4fs', user_func.__name__, total)
58 log.debug('un-cached method:%s took %.4fs', user_func.__name__, total)
59 return result
59 return result
60
60
61 key = key_generator(*arg, **kw)
61 key = key_generator(*arg, **kw)
62
62
63 timeout = expiration_time() if expiration_time_is_callable \
63 timeout = expiration_time() if expiration_time_is_callable \
64 else expiration_time
64 else expiration_time
65
65
66 log.debug('Calling cached method:`%s`', user_func.__name__)
66 log.debug('Calling cached method:`%s`', user_func.__name__)
67 return self.get_or_create(key, user_func, timeout, should_cache_fn, (arg, kw))
67 return self.get_or_create(key, user_func, timeout, should_cache_fn, (arg, kw))
68
68
69 def cache_decorator(user_func):
69 def cache_decorator(user_func):
70 if to_str is str:
70 if to_str is str:
71 # backwards compatible
71 # backwards compatible
72 key_generator = function_key_generator(namespace, user_func)
72 key_generator = function_key_generator(namespace, user_func)
73 else:
73 else:
74 key_generator = function_key_generator(namespace, user_func, to_str=to_str)
74 key_generator = function_key_generator(namespace, user_func, to_str=to_str)
75
75
76 def refresh(*arg, **kw):
76 def refresh(*arg, **kw):
77 """
77 """
78 Like invalidate, but regenerates the value instead
78 Like invalidate, but regenerates the value instead
79 """
79 """
80 key = key_generator(*arg, **kw)
80 key = key_generator(*arg, **kw)
81 value = user_func(*arg, **kw)
81 value = user_func(*arg, **kw)
82 self.set(key, value)
82 self.set(key, value)
83 return value
83 return value
84
84
85 def invalidate(*arg, **kw):
85 def invalidate(*arg, **kw):
86 key = key_generator(*arg, **kw)
86 key = key_generator(*arg, **kw)
87 self.delete(key)
87 self.delete(key)
88
88
89 def set_(value, *arg, **kw):
89 def set_(value, *arg, **kw):
90 key = key_generator(*arg, **kw)
90 key = key_generator(*arg, **kw)
91 self.set(key, value)
91 self.set(key, value)
92
92
93 def get(*arg, **kw):
93 def get(*arg, **kw):
94 key = key_generator(*arg, **kw)
94 key = key_generator(*arg, **kw)
95 return self.get(key)
95 return self.get(key)
96
96
97 user_func.set = set_
97 user_func.set = set_
98 user_func.invalidate = invalidate
98 user_func.invalidate = invalidate
99 user_func.get = get
99 user_func.get = get
100 user_func.refresh = refresh
100 user_func.refresh = refresh
101 user_func.key_generator = key_generator
101 user_func.key_generator = key_generator
102 user_func.original = user_func
102 user_func.original = user_func
103
103
104 # Use `decorate` to preserve the signature of :param:`user_func`.
104 # Use `decorate` to preserve the signature of :param:`user_func`.
105 return decorator.decorate(user_func, functools.partial(
105 return decorator.decorate(user_func, functools.partial(
106 get_or_create_for_user_func, key_generator))
106 get_or_create_for_user_func, key_generator))
107
107
108 return cache_decorator
108 return cache_decorator
109
109
110
110
111 def make_region(*arg, **kw):
111 def make_region(*arg, **kw):
112 return RhodeCodeCacheRegion(*arg, **kw)
112 return RhodeCodeCacheRegion(*arg, **kw)
113
113
114
114
115 def get_default_cache_settings(settings, prefixes=None):
115 def get_default_cache_settings(settings, prefixes=None):
116 prefixes = prefixes or []
116 prefixes = prefixes or []
117 cache_settings = {}
117 cache_settings = {}
118 for key in settings.keys():
118 for key in settings.keys():
119 for prefix in prefixes:
119 for prefix in prefixes:
120 if key.startswith(prefix):
120 if key.startswith(prefix):
121 name = key.split(prefix)[1].strip()
121 name = key.split(prefix)[1].strip()
122 val = settings[key]
122 val = settings[key]
123 if isinstance(val, str):
123 if isinstance(val, str):
124 val = val.strip()
124 val = val.strip()
125 cache_settings[name] = val
125 cache_settings[name] = val
126 return cache_settings
126 return cache_settings
127
127
128
128
129 def compute_key_from_params(*args):
129 def compute_key_from_params(*args):
130 """
130 """
131 Helper to compute key from given params to be used in cache manager
131 Helper to compute key from given params to be used in cache manager
132 """
132 """
133 return sha1("_".join(map(safe_str, args)))
133 return sha1(safe_bytes("_".join(map(str, args))))
134
134
135
135
136 def backend_key_generator(backend):
136 def backend_key_generator(backend):
137 """
137 """
138 Special wrapper that also sends over the backend to the key generator
138 Special wrapper that also sends over the backend to the key generator
139 """
139 """
140 def wrapper(namespace, fn):
140 def wrapper(namespace, fn):
141 return key_generator(backend, namespace, fn)
141 return key_generator(backend, namespace, fn)
142 return wrapper
142 return wrapper
143
143
144
144
145 def key_generator(backend, namespace, fn):
145 def key_generator(backend, namespace, fn):
146 fname = fn.__name__
146 fname = fn.__name__
147
147
148 def generate_key(*args):
148 def generate_key(*args):
149 backend_prefix = getattr(backend, 'key_prefix', None) or 'backend_prefix'
149 backend_prefix = getattr(backend, 'key_prefix', None) or 'backend_prefix'
150 namespace_pref = namespace or 'default_namespace'
150 namespace_pref = namespace or 'default_namespace'
151 arg_key = compute_key_from_params(*args)
151 arg_key = compute_key_from_params(*args)
152 final_key = "{}:{}:{}_{}".format(backend_prefix, namespace_pref, fname, arg_key)
152 final_key = "{}:{}:{}_{}".format(backend_prefix, namespace_pref, fname, arg_key)
153
153
154 return final_key
154 return final_key
155
155
156 return generate_key
156 return generate_key
157
157
158
158
159 def get_or_create_region(region_name, region_namespace=None):
159 def get_or_create_region(region_name, region_namespace=None):
160 from vcsserver.lib.rc_cache.backends import FileNamespaceBackend
160 from vcsserver.lib.rc_cache.backends import FileNamespaceBackend
161 region_obj = region_meta.dogpile_cache_regions.get(region_name)
161 region_obj = region_meta.dogpile_cache_regions.get(region_name)
162 if not region_obj:
162 if not region_obj:
163 raise EnvironmentError(
163 raise EnvironmentError(
164 'Region `{}` not in configured: {}.'.format(
164 'Region `{}` not in configured: {}.'.format(
165 region_name, region_meta.dogpile_cache_regions.keys()))
165 region_name, region_meta.dogpile_cache_regions.keys()))
166
166
167 region_uid_name = '{}:{}'.format(region_name, region_namespace)
167 region_uid_name = '{}:{}'.format(region_name, region_namespace)
168 if isinstance(region_obj.actual_backend, FileNamespaceBackend):
168 if isinstance(region_obj.actual_backend, FileNamespaceBackend):
169 region_exist = region_meta.dogpile_cache_regions.get(region_namespace)
169 region_exist = region_meta.dogpile_cache_regions.get(region_namespace)
170 if region_exist:
170 if region_exist:
171 log.debug('Using already configured region: %s', region_namespace)
171 log.debug('Using already configured region: %s', region_namespace)
172 return region_exist
172 return region_exist
173 cache_dir = region_meta.dogpile_config_defaults['cache_dir']
173 cache_dir = region_meta.dogpile_config_defaults['cache_dir']
174 expiration_time = region_obj.expiration_time
174 expiration_time = region_obj.expiration_time
175
175
176 if not os.path.isdir(cache_dir):
176 if not os.path.isdir(cache_dir):
177 os.makedirs(cache_dir)
177 os.makedirs(cache_dir)
178 new_region = make_region(
178 new_region = make_region(
179 name=region_uid_name,
179 name=region_uid_name,
180 function_key_generator=backend_key_generator(region_obj.actual_backend)
180 function_key_generator=backend_key_generator(region_obj.actual_backend)
181 )
181 )
182 namespace_filename = os.path.join(
182 namespace_filename = os.path.join(
183 cache_dir, "{}.cache.dbm".format(region_namespace))
183 cache_dir, "{}.cache.dbm".format(region_namespace))
184 # special type that allows 1db per namespace
184 # special type that allows 1db per namespace
185 new_region.configure(
185 new_region.configure(
186 backend='dogpile.cache.rc.file_namespace',
186 backend='dogpile.cache.rc.file_namespace',
187 expiration_time=expiration_time,
187 expiration_time=expiration_time,
188 arguments={"filename": namespace_filename}
188 arguments={"filename": namespace_filename}
189 )
189 )
190
190
191 # create and save in region caches
191 # create and save in region caches
192 log.debug('configuring new region: %s', region_uid_name)
192 log.debug('configuring new region: %s', region_uid_name)
193 region_obj = region_meta.dogpile_cache_regions[region_namespace] = new_region
193 region_obj = region_meta.dogpile_cache_regions[region_namespace] = new_region
194
194
195 return region_obj
195 return region_obj
196
196
197
197
198 def clear_cache_namespace(cache_region, cache_namespace_uid, invalidate=False):
198 def clear_cache_namespace(cache_region, cache_namespace_uid, invalidate=False):
199 region = get_or_create_region(cache_region, cache_namespace_uid)
199 region = get_or_create_region(cache_region, cache_namespace_uid)
200 cache_keys = region.backend.list_keys(prefix=cache_namespace_uid)
200 cache_keys = region.backend.list_keys(prefix=cache_namespace_uid)
201 num_delete_keys = len(cache_keys)
201 num_delete_keys = len(cache_keys)
202 if invalidate:
202 if invalidate:
203 region.invalidate(hard=False)
203 region.invalidate(hard=False)
204 else:
204 else:
205 if num_delete_keys:
205 if num_delete_keys:
206 region.delete_multi(cache_keys)
206 region.delete_multi(cache_keys)
207 return num_delete_keys
207 return num_delete_keys
@@ -1,386 +1,413 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 """Handles the Git smart protocol."""
18 """Handles the Git smart protocol."""
19
19
20 import os
20 import os
21 import socket
21 import socket
22 import logging
22 import logging
23
23
24 import simplejson as json
25 import dulwich.protocol
24 import dulwich.protocol
25 from dulwich.protocol import CAPABILITY_SIDE_BAND, CAPABILITY_SIDE_BAND_64K
26 from webob import Request, Response, exc
26 from webob import Request, Response, exc
27
27
28 from vcsserver.lib.rc_json import json
28 from vcsserver import hooks, subprocessio
29 from vcsserver import hooks, subprocessio
30 from vcsserver.utils import ascii_bytes
29
31
30
32
31 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
32
34
33
35
34 class FileWrapper(object):
36 class FileWrapper(object):
35 """File wrapper that ensures how much data is read from it."""
37 """File wrapper that ensures how much data is read from it."""
36
38
37 def __init__(self, fd, content_length):
39 def __init__(self, fd, content_length):
38 self.fd = fd
40 self.fd = fd
39 self.content_length = content_length
41 self.content_length = content_length
40 self.remain = content_length
42 self.remain = content_length
41
43
42 def read(self, size):
44 def read(self, size):
43 if size <= self.remain:
45 if size <= self.remain:
44 try:
46 try:
45 data = self.fd.read(size)
47 data = self.fd.read(size)
46 except socket.error:
48 except socket.error:
47 raise IOError(self)
49 raise IOError(self)
48 self.remain -= size
50 self.remain -= size
49 elif self.remain:
51 elif self.remain:
50 data = self.fd.read(self.remain)
52 data = self.fd.read(self.remain)
51 self.remain = 0
53 self.remain = 0
52 else:
54 else:
53 data = None
55 data = None
54 return data
56 return data
55
57
56 def __repr__(self):
58 def __repr__(self):
57 return '<FileWrapper %s len: %s, read: %s>' % (
59 return '<FileWrapper %s len: %s, read: %s>' % (
58 self.fd, self.content_length, self.content_length - self.remain
60 self.fd, self.content_length, self.content_length - self.remain
59 )
61 )
60
62
61
63
62 class GitRepository(object):
64 class GitRepository(object):
63 """WSGI app for handling Git smart protocol endpoints."""
65 """WSGI app for handling Git smart protocol endpoints."""
64
66
65 git_folder_signature = frozenset(
67 git_folder_signature = frozenset(('config', 'head', 'info', 'objects', 'refs'))
66 ('config', 'head', 'info', 'objects', 'refs'))
67 commands = frozenset(('git-upload-pack', 'git-receive-pack'))
68 commands = frozenset(('git-upload-pack', 'git-receive-pack'))
68 valid_accepts = frozenset(('application/x-%s-result' %
69 valid_accepts = frozenset(('application/x-{}-result'.format(c) for c in commands))
69 c for c in commands))
70
70
71 # The last bytes are the SHA1 of the first 12 bytes.
71 # The last bytes are the SHA1 of the first 12 bytes.
72 EMPTY_PACK = (
72 EMPTY_PACK = (
73 'PACK\x00\x00\x00\x02\x00\x00\x00\x00' +
73 b'PACK\x00\x00\x00\x02\x00\x00\x00\x00\x02\x9d\x08' +
74 '\x02\x9d\x08\x82;\xd8\xa8\xea\xb5\x10\xadj\xc7\\\x82<\xfd>\xd3\x1e'
74 b'\x82;\xd8\xa8\xea\xb5\x10\xadj\xc7\\\x82<\xfd>\xd3\x1e'
75 )
75 )
76 SIDE_BAND_CAPS = frozenset(('side-band', 'side-band-64k'))
76 FLUSH_PACKET = b"0000"
77
77
78 def __init__(self, repo_name, content_path, git_path, update_server_info,
78 SIDE_BAND_CAPS = frozenset((CAPABILITY_SIDE_BAND, CAPABILITY_SIDE_BAND_64K))
79 extras):
79
80 def __init__(self, repo_name, content_path, git_path, update_server_info, extras):
80 files = frozenset(f.lower() for f in os.listdir(content_path))
81 files = frozenset(f.lower() for f in os.listdir(content_path))
81 valid_dir_signature = self.git_folder_signature.issubset(files)
82 valid_dir_signature = self.git_folder_signature.issubset(files)
82
83
83 if not valid_dir_signature:
84 if not valid_dir_signature:
84 raise OSError('%s missing git signature' % content_path)
85 raise OSError('%s missing git signature' % content_path)
85
86
86 self.content_path = content_path
87 self.content_path = content_path
87 self.repo_name = repo_name
88 self.repo_name = repo_name
88 self.extras = extras
89 self.extras = extras
89 self.git_path = git_path
90 self.git_path = git_path
90 self.update_server_info = update_server_info
91 self.update_server_info = update_server_info
91
92
92 def _get_fixedpath(self, path):
93 def _get_fixedpath(self, path):
93 """
94 """
94 Small fix for repo_path
95 Small fix for repo_path
95
96
96 :param path:
97 :param path:
97 """
98 """
98 path = path.split(self.repo_name, 1)[-1]
99 path = path.split(self.repo_name, 1)[-1]
99 if path.startswith('.git'):
100 if path.startswith('.git'):
100 # for bare repos we still get the .git prefix inside, we skip it
101 # for bare repos we still get the .git prefix inside, we skip it
101 # here, and remove from the service command
102 # here, and remove from the service command
102 path = path[4:]
103 path = path[4:]
103
104
104 return path.strip('/')
105 return path.strip('/')
105
106
106 def inforefs(self, request, unused_environ):
107 def inforefs(self, request, unused_environ):
107 """
108 """
108 WSGI Response producer for HTTP GET Git Smart
109 WSGI Response producer for HTTP GET Git Smart
109 HTTP /info/refs request.
110 HTTP /info/refs request.
110 """
111 """
111
112
112 git_command = request.GET.get('service')
113 git_command = request.GET.get('service')
113 if git_command not in self.commands:
114 if git_command not in self.commands:
114 log.debug('command %s not allowed', git_command)
115 log.debug('command %s not allowed', git_command)
115 return exc.HTTPForbidden()
116 return exc.HTTPForbidden()
116
117
117 # please, resist the urge to add '\n' to git capture and increment
118 # please, resist the urge to add '\n' to git capture and increment
118 # line count by 1.
119 # line count by 1.
119 # by git docs: Documentation/technical/http-protocol.txt#L214 \n is
120 # by git docs: Documentation/technical/http-protocol.txt#L214 \n is
120 # a part of protocol.
121 # a part of protocol.
121 # The code in Git client not only does NOT need '\n', but actually
122 # The code in Git client not only does NOT need '\n', but actually
122 # blows up if you sprinkle "flush" (0000) as "0001\n".
123 # blows up if you sprinkle "flush" (0000) as "0001\n".
123 # It reads binary, per number of bytes specified.
124 # It reads binary, per number of bytes specified.
124 # if you do add '\n' as part of data, count it.
125 # if you do add '\n' as part of data, count it.
125 server_advert = '# service=%s\n' % git_command
126 server_advert = '# service=%s\n' % git_command
126 packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
127 packet_len = hex(len(server_advert) + 4)[2:].rjust(4, '0').lower()
127 try:
128 try:
128 gitenv = dict(os.environ)
129 gitenv = dict(os.environ)
129 # forget all configs
130 # forget all configs
130 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
131 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
131 command = [self.git_path, git_command[4:], '--stateless-rpc',
132 command = [self.git_path, git_command[4:], '--stateless-rpc',
132 '--advertise-refs', self.content_path]
133 '--advertise-refs', self.content_path]
133 out = subprocessio.SubprocessIOChunker(
134 out = subprocessio.SubprocessIOChunker(
134 command,
135 command,
135 env=gitenv,
136 env=gitenv,
136 starting_values=[packet_len + server_advert + '0000'],
137 starting_values=[ascii_bytes(packet_len + server_advert) + self.FLUSH_PACKET],
137 shell=False
138 shell=False
138 )
139 )
139 except EnvironmentError:
140 except OSError:
140 log.exception('Error processing command')
141 log.exception('Error processing command')
141 raise exc.HTTPExpectationFailed()
142 raise exc.HTTPExpectationFailed()
142
143
143 resp = Response()
144 resp = Response()
144 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
145 resp.content_type = f'application/x-{git_command}-advertisement'
145 resp.charset = None
146 resp.charset = None
146 resp.app_iter = out
147 resp.app_iter = out
147
148
148 return resp
149 return resp
149
150
150 def _get_want_capabilities(self, request):
151 def _get_want_capabilities(self, request):
151 """Read the capabilities found in the first want line of the request."""
152 """Read the capabilities found in the first want line of the request."""
152 pos = request.body_file_seekable.tell()
153 pos = request.body_file_seekable.tell()
153 first_line = request.body_file_seekable.readline()
154 first_line = request.body_file_seekable.readline()
154 request.body_file_seekable.seek(pos)
155 request.body_file_seekable.seek(pos)
155
156
156 return frozenset(
157 return frozenset(
157 dulwich.protocol.extract_want_line_capabilities(first_line)[1])
158 dulwich.protocol.extract_want_line_capabilities(first_line)[1])
158
159
159 def _build_failed_pre_pull_response(self, capabilities, pre_pull_messages):
160 def _build_failed_pre_pull_response(self, capabilities, pre_pull_messages):
160 """
161 """
161 Construct a response with an empty PACK file.
162 Construct a response with an empty PACK file.
162
163
163 We use an empty PACK file, as that would trigger the failure of the pull
164 We use an empty PACK file, as that would trigger the failure of the pull
164 or clone command.
165 or clone command.
165
166
166 We also print in the error output a message explaining why the command
167 We also print in the error output a message explaining why the command
167 was aborted.
168 was aborted.
168
169
169 If aditionally, the user is accepting messages we send them the output
170 If additionally, the user is accepting messages we send them the output
170 of the pre-pull hook.
171 of the pre-pull hook.
171
172
172 Note that for clients not supporting side-band we just send them the
173 Note that for clients not supporting side-band we just send them the
173 emtpy PACK file.
174 emtpy PACK file.
174 """
175 """
176
175 if self.SIDE_BAND_CAPS.intersection(capabilities):
177 if self.SIDE_BAND_CAPS.intersection(capabilities):
176 response = []
178 response = []
177 proto = dulwich.protocol.Protocol(None, response.append)
179 proto = dulwich.protocol.Protocol(None, response.append)
178 proto.write_pkt_line('NAK\n')
180 proto.write_pkt_line(dulwich.protocol.NAK_LINE)
179 self._write_sideband_to_proto(pre_pull_messages, proto,
181
180 capabilities)
182 self._write_sideband_to_proto(proto, ascii_bytes(pre_pull_messages, allow_bytes=True), capabilities)
181 # N.B.(skreft): Do not change the sideband channel to 3, as that
183 # N.B.(skreft): Do not change the sideband channel to 3, as that
182 # produces a fatal error in the client:
184 # produces a fatal error in the client:
183 # fatal: error in sideband demultiplexer
185 # fatal: error in sideband demultiplexer
184 proto.write_sideband(2, 'Pre pull hook failed: aborting\n')
186 proto.write_sideband(
185 proto.write_sideband(1, self.EMPTY_PACK)
187 dulwich.protocol.SIDE_BAND_CHANNEL_PROGRESS,
188 ascii_bytes('Pre pull hook failed: aborting\n', allow_bytes=True))
189 proto.write_sideband(
190 dulwich.protocol.SIDE_BAND_CHANNEL_DATA,
191 ascii_bytes(self.EMPTY_PACK, allow_bytes=True))
186
192
187 # writes 0000
193 # writes b"0000" as default
188 proto.write_pkt_line(None)
194 proto.write_pkt_line(None)
189
195
190 return response
196 return response
191 else:
197 else:
192 return [self.EMPTY_PACK]
198 return [ascii_bytes(self.EMPTY_PACK, allow_bytes=True)]
199
200 def _build_post_pull_response(self, response, capabilities, start_message, end_message):
201 """
202 Given a list response we inject the post-pull messages.
203
204 We only inject the messages if the client supports sideband, and the
205 response has the format:
206 0008NAK\n...0000
207
208 Note that we do not check the no-progress capability as by default, git
209 sends it, which effectively would block all messages.
210 """
211
212 if not self.SIDE_BAND_CAPS.intersection(capabilities):
213 return response
214
215 if not start_message and not end_message:
216 return response
217
218 try:
219 iter(response)
220 # iterator probably will work, we continue
221 except TypeError:
222 raise TypeError(f'response must be an iterator: got {type(response)}')
223 if isinstance(response, (list, tuple)):
224 raise TypeError(f'response must be an iterator: got {type(response)}')
225
226 def injected_response():
193
227
194 def _write_sideband_to_proto(self, data, proto, capabilities):
228 do_loop = 1
229 header_injected = 0
230 next_item = None
231 has_item = False
232 while do_loop:
233
234 try:
235 next_item = next(response)
236 except StopIteration:
237 do_loop = 0
238
239 if has_item:
240 # last item ! alter it now
241 if do_loop == 0 and item.endswith(self.FLUSH_PACKET):
242 new_response = [item[:-4]]
243 new_response.extend(self._get_messages(end_message, capabilities))
244 new_response.append(self.FLUSH_PACKET)
245 item = b''.join(new_response)
246
247 yield item
248 has_item = True
249 item = next_item
250
251 # alter item if it's the initial chunk
252 if not header_injected and item.startswith(b'0008NAK\n'):
253 new_response = [b'0008NAK\n']
254 new_response.extend(self._get_messages(start_message, capabilities))
255 new_response.append(item[8:])
256 item = b''.join(new_response)
257 header_injected = 1
258
259 return injected_response()
260
261 def _write_sideband_to_proto(self, proto, data, capabilities):
195 """
262 """
196 Write the data to the proto's sideband number 2.
263 Write the data to the proto's sideband number 2 == SIDE_BAND_CHANNEL_PROGRESS
197
264
198 We do not use dulwich's write_sideband directly as it only supports
265 We do not use dulwich's write_sideband directly as it only supports
199 side-band-64k.
266 side-band-64k.
200 """
267 """
201 if not data:
268 if not data:
202 return
269 return
203
270
204 # N.B.(skreft): The values below are explained in the pack protocol
271 # N.B.(skreft): The values below are explained in the pack protocol
205 # documentation, section Packfile Data.
272 # documentation, section Packfile Data.
206 # https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt
273 # https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt
207 if 'side-band-64k' in capabilities:
274 if CAPABILITY_SIDE_BAND_64K in capabilities:
208 chunk_size = 65515
275 chunk_size = 65515
209 elif 'side-band' in capabilities:
276 elif CAPABILITY_SIDE_BAND in capabilities:
210 chunk_size = 995
277 chunk_size = 995
211 else:
278 else:
212 return
279 return
213
280
214 chunker = (
281 chunker = (data[i:i + chunk_size] for i in range(0, len(data), chunk_size))
215 data[i:i + chunk_size] for i in range(0, len(data), chunk_size))
216
282
217 for chunk in chunker:
283 for chunk in chunker:
218 proto.write_sideband(2, chunk)
284 proto.write_sideband(dulwich.protocol.SIDE_BAND_CHANNEL_PROGRESS, ascii_bytes(chunk, allow_bytes=True))
219
285
220 def _get_messages(self, data, capabilities):
286 def _get_messages(self, data, capabilities):
221 """Return a list with packets for sending data in sideband number 2."""
287 """Return a list with packets for sending data in sideband number 2."""
222 response = []
288 response = []
223 proto = dulwich.protocol.Protocol(None, response.append)
289 proto = dulwich.protocol.Protocol(None, response.append)
224
290
225 self._write_sideband_to_proto(data, proto, capabilities)
291 self._write_sideband_to_proto(proto, data, capabilities)
226
292
227 return response
293 return response
228
294
229 def _inject_messages_to_response(self, response, capabilities,
230 start_messages, end_messages):
231 """
232 Given a list response we inject the pre/post-pull messages.
233
234 We only inject the messages if the client supports sideband, and the
235 response has the format:
236 0008NAK\n...0000
237
238 Note that we do not check the no-progress capability as by default, git
239 sends it, which effectively would block all messages.
240 """
241 if not self.SIDE_BAND_CAPS.intersection(capabilities):
242 return response
243
244 if not start_messages and not end_messages:
245 return response
246
247 # make a list out of response if it's an iterator
248 # so we can investigate it for message injection.
249 if hasattr(response, '__iter__'):
250 response = list(response)
251
252 if (not response[0].startswith('0008NAK\n') or
253 not response[-1].endswith('0000')):
254 return response
255
256 new_response = ['0008NAK\n']
257 new_response.extend(self._get_messages(start_messages, capabilities))
258 if len(response) == 1:
259 new_response.append(response[0][8:-4])
260 else:
261 new_response.append(response[0][8:])
262 new_response.extend(response[1:-1])
263 new_response.append(response[-1][:-4])
264 new_response.extend(self._get_messages(end_messages, capabilities))
265 new_response.append('0000')
266
267 return new_response
268
269 def backend(self, request, environ):
295 def backend(self, request, environ):
270 """
296 """
271 WSGI Response producer for HTTP POST Git Smart HTTP requests.
297 WSGI Response producer for HTTP POST Git Smart HTTP requests.
272 Reads commands and data from HTTP POST's body.
298 Reads commands and data from HTTP POST's body.
273 returns an iterator obj with contents of git command's
299 returns an iterator obj with contents of git command's
274 response to stdout
300 response to stdout
275 """
301 """
276 # TODO(skreft): think how we could detect an HTTPLockedException, as
302 # TODO(skreft): think how we could detect an HTTPLockedException, as
277 # we probably want to have the same mechanism used by mercurial and
303 # we probably want to have the same mechanism used by mercurial and
278 # simplevcs.
304 # simplevcs.
279 # For that we would need to parse the output of the command looking for
305 # For that we would need to parse the output of the command looking for
280 # some signs of the HTTPLockedError, parse the data and reraise it in
306 # some signs of the HTTPLockedError, parse the data and reraise it in
281 # pygrack. However, that would interfere with the streaming.
307 # pygrack. However, that would interfere with the streaming.
282 #
308 #
283 # Now the output of a blocked push is:
309 # Now the output of a blocked push is:
284 # Pushing to http://test_regular:test12@127.0.0.1:5001/vcs_test_git
310 # Pushing to http://test_regular:test12@127.0.0.1:5001/vcs_test_git
285 # POST git-receive-pack (1047 bytes)
311 # POST git-receive-pack (1047 bytes)
286 # remote: ERROR: Repository `vcs_test_git` locked by user `test_admin`. Reason:`lock_auto`
312 # remote: ERROR: Repository `vcs_test_git` locked by user `test_admin`. Reason:`lock_auto`
287 # To http://test_regular:test12@127.0.0.1:5001/vcs_test_git
313 # To http://test_regular:test12@127.0.0.1:5001/vcs_test_git
288 # ! [remote rejected] master -> master (pre-receive hook declined)
314 # ! [remote rejected] master -> master (pre-receive hook declined)
289 # error: failed to push some refs to 'http://test_regular:test12@127.0.0.1:5001/vcs_test_git'
315 # error: failed to push some refs to 'http://test_regular:test12@127.0.0.1:5001/vcs_test_git'
290
316
291 git_command = self._get_fixedpath(request.path_info)
317 git_command = self._get_fixedpath(request.path_info)
292 if git_command not in self.commands:
318 if git_command not in self.commands:
293 log.debug('command %s not allowed', git_command)
319 log.debug('command %s not allowed', git_command)
294 return exc.HTTPForbidden()
320 return exc.HTTPForbidden()
295
321
296 capabilities = None
322 capabilities = None
297 if git_command == 'git-upload-pack':
323 if git_command == 'git-upload-pack':
298 capabilities = self._get_want_capabilities(request)
324 capabilities = self._get_want_capabilities(request)
299
325
300 if 'CONTENT_LENGTH' in environ:
326 if 'CONTENT_LENGTH' in environ:
301 inputstream = FileWrapper(request.body_file_seekable,
327 inputstream = FileWrapper(request.body_file_seekable,
302 request.content_length)
328 request.content_length)
303 else:
329 else:
304 inputstream = request.body_file_seekable
330 inputstream = request.body_file_seekable
305
331
306 resp = Response()
332 resp = Response()
307 resp.content_type = ('application/x-%s-result' %
333 resp.content_type = 'application/x-{}-result'.format(git_command)
308 git_command.encode('utf8'))
309 resp.charset = None
334 resp.charset = None
310
335
311 pre_pull_messages = ''
336 pre_pull_messages = ''
337 # Upload-pack == clone
312 if git_command == 'git-upload-pack':
338 if git_command == 'git-upload-pack':
313 status, pre_pull_messages = hooks.git_pre_pull(self.extras)
339 status, pre_pull_messages = hooks.git_pre_pull(self.extras)
314 if status != 0:
340 if status != 0:
315 resp.app_iter = self._build_failed_pre_pull_response(
341 resp.app_iter = self._build_failed_pre_pull_response(
316 capabilities, pre_pull_messages)
342 capabilities, pre_pull_messages)
317 return resp
343 return resp
318
344
319 gitenv = dict(os.environ)
345 gitenv = dict(os.environ)
320 # forget all configs
346 # forget all configs
321 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
347 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
322 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
348 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
323 cmd = [self.git_path, git_command[4:], '--stateless-rpc',
349 cmd = [self.git_path, git_command[4:], '--stateless-rpc',
324 self.content_path]
350 self.content_path]
325 log.debug('handling cmd %s', cmd)
351 log.debug('handling cmd %s', cmd)
326
352
327 out = subprocessio.SubprocessIOChunker(
353 out = subprocessio.SubprocessIOChunker(
328 cmd,
354 cmd,
329 inputstream=inputstream,
355 input_stream=inputstream,
330 env=gitenv,
356 env=gitenv,
331 cwd=self.content_path,
357 cwd=self.content_path,
332 shell=False,
358 shell=False,
333 fail_on_stderr=False,
359 fail_on_stderr=False,
334 fail_on_return_code=False
360 fail_on_return_code=False
335 )
361 )
336
362
337 if self.update_server_info and git_command == 'git-receive-pack':
363 if self.update_server_info and git_command == 'git-receive-pack':
338 # We need to fully consume the iterator here, as the
364 # We need to fully consume the iterator here, as the
339 # update-server-info command needs to be run after the push.
365 # update-server-info command needs to be run after the push.
340 out = list(out)
366 out = list(out)
341
367
342 # Updating refs manually after each push.
368 # Updating refs manually after each push.
343 # This is required as some clients are exposing Git repos internally
369 # This is required as some clients are exposing Git repos internally
344 # with the dumb protocol.
370 # with the dumb protocol.
345 cmd = [self.git_path, 'update-server-info']
371 cmd = [self.git_path, 'update-server-info']
346 log.debug('handling cmd %s', cmd)
372 log.debug('handling cmd %s', cmd)
347 output = subprocessio.SubprocessIOChunker(
373 output = subprocessio.SubprocessIOChunker(
348 cmd,
374 cmd,
349 inputstream=inputstream,
375 input_stream=inputstream,
350 env=gitenv,
376 env=gitenv,
351 cwd=self.content_path,
377 cwd=self.content_path,
352 shell=False,
378 shell=False,
353 fail_on_stderr=False,
379 fail_on_stderr=False,
354 fail_on_return_code=False
380 fail_on_return_code=False
355 )
381 )
356 # Consume all the output so the subprocess finishes
382 # Consume all the output so the subprocess finishes
357 for _ in output:
383 for _ in output:
358 pass
384 pass
359
385
386 # Upload-pack == clone
360 if git_command == 'git-upload-pack':
387 if git_command == 'git-upload-pack':
361 unused_status, post_pull_messages = hooks.git_post_pull(self.extras)
388 unused_status, post_pull_messages = hooks.git_post_pull(self.extras)
362 resp.app_iter = self._inject_messages_to_response(
389
363 out, capabilities, pre_pull_messages, post_pull_messages)
390 resp.app_iter = self._build_post_pull_response(out, capabilities, pre_pull_messages, post_pull_messages)
364 else:
391 else:
365 resp.app_iter = out
392 resp.app_iter = out
366
393
367 return resp
394 return resp
368
395
369 def __call__(self, environ, start_response):
396 def __call__(self, environ, start_response):
370 request = Request(environ)
397 request = Request(environ)
371 _path = self._get_fixedpath(request.path_info)
398 _path = self._get_fixedpath(request.path_info)
372 if _path.startswith('info/refs'):
399 if _path.startswith('info/refs'):
373 app = self.inforefs
400 app = self.inforefs
374 else:
401 else:
375 app = self.backend
402 app = self.backend
376
403
377 try:
404 try:
378 resp = app(request, environ)
405 resp = app(request, environ)
379 except exc.HTTPException as error:
406 except exc.HTTPException as error:
380 log.exception('HTTP Error')
407 log.exception('HTTP Error')
381 resp = error
408 resp = error
382 except Exception:
409 except Exception:
383 log.exception('Unknown error')
410 log.exception('Unknown error')
384 resp = exc.HTTPInternalServerError()
411 resp = exc.HTTPInternalServerError()
385
412
386 return resp(environ, start_response)
413 return resp(environ, start_response)
@@ -1,1281 +1,1317 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import collections
18 import collections
19 import logging
19 import logging
20 import os
20 import os
21 import posixpath as vcspath
21 import posixpath as vcspath
22 import re
22 import re
23 import stat
23 import stat
24 import traceback
24 import traceback
25 import urllib.request, urllib.parse, urllib.error
25 import urllib.request, urllib.parse, urllib.error
26 import urllib.request, urllib.error, urllib.parse
26 import urllib.request, urllib.error, urllib.parse
27 from functools import wraps
27 from functools import wraps
28
28
29 import more_itertools
29 import more_itertools
30 import pygit2
30 import pygit2
31 from pygit2 import Repository as LibGit2Repo
31 from pygit2 import Repository as LibGit2Repo
32 from pygit2 import index as LibGit2Index
32 from pygit2 import index as LibGit2Index
33 from dulwich import index, objects
33 from dulwich import index, objects
34 from dulwich.client import HttpGitClient, LocalGitClient
34 from dulwich.client import HttpGitClient, LocalGitClient
35 from dulwich.errors import (
35 from dulwich.errors import (
36 NotGitRepository, ChecksumMismatch, WrongObjectException,
36 NotGitRepository, ChecksumMismatch, WrongObjectException,
37 MissingCommitError, ObjectMissing, HangupException,
37 MissingCommitError, ObjectMissing, HangupException,
38 UnexpectedCommandError)
38 UnexpectedCommandError)
39 from dulwich.repo import Repo as DulwichRepo
39 from dulwich.repo import Repo as DulwichRepo
40 from dulwich.server import update_server_info
40 from dulwich.server import update_server_info
41
41
42 from vcsserver import exceptions, settings, subprocessio
42 from vcsserver import exceptions, settings, subprocessio
43 from vcsserver.utils import safe_str, safe_int, safe_unicode
43 from vcsserver.utils import safe_str, safe_int
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, archive_repo
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, archive_repo
45 from vcsserver.hgcompat import (
45 from vcsserver.hgcompat import (
46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
47 from vcsserver.git_lfs.lib import LFSOidStore
47 from vcsserver.git_lfs.lib import LFSOidStore
48 from vcsserver.vcs_base import RemoteBase
48 from vcsserver.vcs_base import RemoteBase
49
49
50 DIR_STAT = stat.S_IFDIR
50 DIR_STAT = stat.S_IFDIR
51 FILE_MODE = stat.S_IFMT
51 FILE_MODE = stat.S_IFMT
52 GIT_LINK = objects.S_IFGITLINK
52 GIT_LINK = objects.S_IFGITLINK
53 PEELED_REF_MARKER = '^{}'
53 PEELED_REF_MARKER = '^{}'
54
54
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 def str_to_dulwich(value):
60 """
61 Dulwich 0.10.1a requires `unicode` objects to be passed in.
62 """
63 return value.decode(settings.WIRE_ENCODING)
64
65
66 def reraise_safe_exceptions(func):
59 def reraise_safe_exceptions(func):
67 """Converts Dulwich exceptions to something neutral."""
60 """Converts Dulwich exceptions to something neutral."""
68
61
69 @wraps(func)
62 @wraps(func)
70 def wrapper(*args, **kwargs):
63 def wrapper(*args, **kwargs):
71 try:
64 try:
72 return func(*args, **kwargs)
65 return func(*args, **kwargs)
73 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
74 exc = exceptions.LookupException(org_exc=e)
67 exc = exceptions.LookupException(org_exc=e)
75 raise exc(safe_str(e))
68 raise exc(safe_str(e))
76 except (HangupException, UnexpectedCommandError) as e:
69 except (HangupException, UnexpectedCommandError) as e:
77 exc = exceptions.VcsException(org_exc=e)
70 exc = exceptions.VcsException(org_exc=e)
78 raise exc(safe_str(e))
71 raise exc(safe_str(e))
79 except Exception as e:
72 except Exception as e:
80 # NOTE(marcink): becuase of how dulwich handles some exceptions
73 # NOTE(marcink): becuase of how dulwich handles some exceptions
81 # (KeyError on empty repos), we cannot track this and catch all
74 # (KeyError on empty repos), we cannot track this and catch all
82 # exceptions, it's an exceptions from other handlers
75 # exceptions, it's an exceptions from other handlers
83 #if not hasattr(e, '_vcs_kind'):
76 #if not hasattr(e, '_vcs_kind'):
84 #log.exception("Unhandled exception in git remote call")
77 #log.exception("Unhandled exception in git remote call")
85 #raise_from_original(exceptions.UnhandledException)
78 #raise_from_original(exceptions.UnhandledException)
86 raise
79 raise
87 return wrapper
80 return wrapper
88
81
89
82
90 class Repo(DulwichRepo):
83 class Repo(DulwichRepo):
91 """
84 """
92 A wrapper for dulwich Repo class.
85 A wrapper for dulwich Repo class.
93
86
94 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
95 "Too many open files" error. We need to close all opened file descriptors
88 "Too many open files" error. We need to close all opened file descriptors
96 once the repo object is destroyed.
89 once the repo object is destroyed.
97 """
90 """
98 def __del__(self):
91 def __del__(self):
99 if hasattr(self, 'object_store'):
92 if hasattr(self, 'object_store'):
100 self.close()
93 self.close()
101
94
102
95
103 class Repository(LibGit2Repo):
96 class Repository(LibGit2Repo):
104
97
105 def __enter__(self):
98 def __enter__(self):
106 return self
99 return self
107
100
108 def __exit__(self, exc_type, exc_val, exc_tb):
101 def __exit__(self, exc_type, exc_val, exc_tb):
109 self.free()
102 self.free()
110
103
111
104
112 class GitFactory(RepoFactory):
105 class GitFactory(RepoFactory):
113 repo_type = 'git'
106 repo_type = 'git'
114
107
115 def _create_repo(self, wire, create, use_libgit2=False):
108 def _create_repo(self, wire, create, use_libgit2=False):
116 if use_libgit2:
109 if use_libgit2:
117 return Repository(wire['path'])
110 return Repository(wire['path'])
118 else:
111 else:
119 repo_path = str_to_dulwich(wire['path'])
112 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
120 return Repo(repo_path)
113 return Repo(repo_path)
121
114
122 def repo(self, wire, create=False, use_libgit2=False):
115 def repo(self, wire, create=False, use_libgit2=False):
123 """
116 """
124 Get a repository instance for the given path.
117 Get a repository instance for the given path.
125 """
118 """
126 return self._create_repo(wire, create, use_libgit2)
119 return self._create_repo(wire, create, use_libgit2)
127
120
128 def repo_libgit2(self, wire):
121 def repo_libgit2(self, wire):
129 return self.repo(wire, use_libgit2=True)
122 return self.repo(wire, use_libgit2=True)
130
123
131
124
132 class GitRemote(RemoteBase):
125 class GitRemote(RemoteBase):
133
126
134 def __init__(self, factory):
127 def __init__(self, factory):
135 self._factory = factory
128 self._factory = factory
136 self._bulk_methods = {
129 self._bulk_methods = {
137 "date": self.date,
130 "date": self.date,
138 "author": self.author,
131 "author": self.author,
139 "branch": self.branch,
132 "branch": self.branch,
140 "message": self.message,
133 "message": self.message,
141 "parents": self.parents,
134 "parents": self.parents,
142 "_commit": self.revision,
135 "_commit": self.revision,
143 }
136 }
144
137
145 def _wire_to_config(self, wire):
138 def _wire_to_config(self, wire):
146 if 'config' in wire:
139 if 'config' in wire:
147 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
140 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
148 return {}
141 return {}
149
142
150 def _remote_conf(self, config):
143 def _remote_conf(self, config):
151 params = [
144 params = [
152 '-c', 'core.askpass=""',
145 '-c', 'core.askpass=""',
153 ]
146 ]
154 ssl_cert_dir = config.get('vcs_ssl_dir')
147 ssl_cert_dir = config.get('vcs_ssl_dir')
155 if ssl_cert_dir:
148 if ssl_cert_dir:
156 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
149 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
157 return params
150 return params
158
151
159 @reraise_safe_exceptions
152 @reraise_safe_exceptions
160 def discover_git_version(self):
153 def discover_git_version(self):
161 stdout, _ = self.run_git_command(
154 stdout, _ = self.run_git_command(
162 {}, ['--version'], _bare=True, _safe=True)
155 {}, ['--version'], _bare=True, _safe=True)
163 prefix = 'git version'
156 prefix = b'git version'
164 if stdout.startswith(prefix):
157 if stdout.startswith(prefix):
165 stdout = stdout[len(prefix):]
158 stdout = stdout[len(prefix):]
166 return stdout.strip()
159 return stdout.strip()
167
160
168 @reraise_safe_exceptions
161 @reraise_safe_exceptions
169 def is_empty(self, wire):
162 def is_empty(self, wire):
170 repo_init = self._factory.repo_libgit2(wire)
163 repo_init = self._factory.repo_libgit2(wire)
171 with repo_init as repo:
164 with repo_init as repo:
172
165
173 try:
166 try:
174 has_head = repo.head.name
167 has_head = repo.head.name
175 if has_head:
168 if has_head:
176 return False
169 return False
177
170
178 # NOTE(marcink): check again using more expensive method
171 # NOTE(marcink): check again using more expensive method
179 return repo.is_empty
172 return repo.is_empty
180 except Exception:
173 except Exception:
181 pass
174 pass
182
175
183 return True
176 return True
184
177
185 @reraise_safe_exceptions
178 @reraise_safe_exceptions
186 def assert_correct_path(self, wire):
179 def assert_correct_path(self, wire):
187 cache_on, context_uid, repo_id = self._cache_on(wire)
180 cache_on, context_uid, repo_id = self._cache_on(wire)
188 region = self._region(wire)
181 region = self._region(wire)
182
189 @region.conditional_cache_on_arguments(condition=cache_on)
183 @region.conditional_cache_on_arguments(condition=cache_on)
190 def _assert_correct_path(_context_uid, _repo_id):
184 def _assert_correct_path(_context_uid, _repo_id):
191 try:
185 try:
192 repo_init = self._factory.repo_libgit2(wire)
186 repo_init = self._factory.repo_libgit2(wire)
193 with repo_init as repo:
187 with repo_init as repo:
194 pass
188 pass
195 except pygit2.GitError:
189 except pygit2.GitError:
196 path = wire.get('path')
190 path = wire.get('path')
197 tb = traceback.format_exc()
191 tb = traceback.format_exc()
198 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
192 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
199 return False
193 return False
200
194
201 return True
195 return True
202 return _assert_correct_path(context_uid, repo_id)
196 return _assert_correct_path(context_uid, repo_id)
203
197
204 @reraise_safe_exceptions
198 @reraise_safe_exceptions
205 def bare(self, wire):
199 def bare(self, wire):
206 repo_init = self._factory.repo_libgit2(wire)
200 repo_init = self._factory.repo_libgit2(wire)
207 with repo_init as repo:
201 with repo_init as repo:
208 return repo.is_bare
202 return repo.is_bare
209
203
210 @reraise_safe_exceptions
204 @reraise_safe_exceptions
211 def blob_as_pretty_string(self, wire, sha):
205 def blob_as_pretty_string(self, wire, sha):
212 repo_init = self._factory.repo_libgit2(wire)
206 repo_init = self._factory.repo_libgit2(wire)
213 with repo_init as repo:
207 with repo_init as repo:
214 blob_obj = repo[sha]
208 blob_obj = repo[sha]
215 blob = blob_obj.data
209 blob = blob_obj.data
216 return blob
210 return blob
217
211
218 @reraise_safe_exceptions
212 @reraise_safe_exceptions
219 def blob_raw_length(self, wire, sha):
213 def blob_raw_length(self, wire, sha):
220 cache_on, context_uid, repo_id = self._cache_on(wire)
214 cache_on, context_uid, repo_id = self._cache_on(wire)
221 region = self._region(wire)
215 region = self._region(wire)
216
222 @region.conditional_cache_on_arguments(condition=cache_on)
217 @region.conditional_cache_on_arguments(condition=cache_on)
223 def _blob_raw_length(_repo_id, _sha):
218 def _blob_raw_length(_repo_id, _sha):
224
219
225 repo_init = self._factory.repo_libgit2(wire)
220 repo_init = self._factory.repo_libgit2(wire)
226 with repo_init as repo:
221 with repo_init as repo:
227 blob = repo[sha]
222 blob = repo[sha]
228 return blob.size
223 return blob.size
229
224
230 return _blob_raw_length(repo_id, sha)
225 return _blob_raw_length(repo_id, sha)
231
226
232 def _parse_lfs_pointer(self, raw_content):
227 def _parse_lfs_pointer(self, raw_content):
228 spec_string = b'version https://git-lfs.github.com/spec'
229 if raw_content and raw_content.startswith(spec_string):
233
230
234 spec_string = 'version https://git-lfs.github.com/spec'
231 pattern = re.compile(rb"""
235 if raw_content and raw_content.startswith(spec_string):
236 pattern = re.compile(r"""
237 (?:\n)?
232 (?:\n)?
238 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
233 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
239 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
234 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
240 ^size[ ](?P<oid_size>[0-9]+)\n
235 ^size[ ](?P<oid_size>[0-9]+)\n
241 (?:\n)?
236 (?:\n)?
242 """, re.VERBOSE | re.MULTILINE)
237 """, re.VERBOSE | re.MULTILINE)
243 match = pattern.match(raw_content)
238 match = pattern.match(raw_content)
244 if match:
239 if match:
245 return match.groupdict()
240 return match.groupdict()
246
241
247 return {}
242 return {}
248
243
249 @reraise_safe_exceptions
244 @reraise_safe_exceptions
250 def is_large_file(self, wire, commit_id):
245 def is_large_file(self, wire, commit_id):
251 cache_on, context_uid, repo_id = self._cache_on(wire)
246 cache_on, context_uid, repo_id = self._cache_on(wire)
247 region = self._region(wire)
252
248
253 region = self._region(wire)
254 @region.conditional_cache_on_arguments(condition=cache_on)
249 @region.conditional_cache_on_arguments(condition=cache_on)
255 def _is_large_file(_repo_id, _sha):
250 def _is_large_file(_repo_id, _sha):
256 repo_init = self._factory.repo_libgit2(wire)
251 repo_init = self._factory.repo_libgit2(wire)
257 with repo_init as repo:
252 with repo_init as repo:
258 blob = repo[commit_id]
253 blob = repo[commit_id]
259 if blob.is_binary:
254 if blob.is_binary:
260 return {}
255 return {}
261
256
262 return self._parse_lfs_pointer(blob.data)
257 return self._parse_lfs_pointer(blob.data)
263
258
264 return _is_large_file(repo_id, commit_id)
259 return _is_large_file(repo_id, commit_id)
265
260
266 @reraise_safe_exceptions
261 @reraise_safe_exceptions
267 def is_binary(self, wire, tree_id):
262 def is_binary(self, wire, tree_id):
268 cache_on, context_uid, repo_id = self._cache_on(wire)
263 cache_on, context_uid, repo_id = self._cache_on(wire)
264 region = self._region(wire)
269
265
270 region = self._region(wire)
271 @region.conditional_cache_on_arguments(condition=cache_on)
266 @region.conditional_cache_on_arguments(condition=cache_on)
272 def _is_binary(_repo_id, _tree_id):
267 def _is_binary(_repo_id, _tree_id):
273 repo_init = self._factory.repo_libgit2(wire)
268 repo_init = self._factory.repo_libgit2(wire)
274 with repo_init as repo:
269 with repo_init as repo:
275 blob_obj = repo[tree_id]
270 blob_obj = repo[tree_id]
276 return blob_obj.is_binary
271 return blob_obj.is_binary
277
272
278 return _is_binary(repo_id, tree_id)
273 return _is_binary(repo_id, tree_id)
279
274
280 @reraise_safe_exceptions
275 @reraise_safe_exceptions
281 def in_largefiles_store(self, wire, oid):
276 def in_largefiles_store(self, wire, oid):
282 conf = self._wire_to_config(wire)
277 conf = self._wire_to_config(wire)
283 repo_init = self._factory.repo_libgit2(wire)
278 repo_init = self._factory.repo_libgit2(wire)
284 with repo_init as repo:
279 with repo_init as repo:
285 repo_name = repo.path
280 repo_name = repo.path
286
281
287 store_location = conf.get('vcs_git_lfs_store_location')
282 store_location = conf.get('vcs_git_lfs_store_location')
288 if store_location:
283 if store_location:
289
284
290 store = LFSOidStore(
285 store = LFSOidStore(
291 oid=oid, repo=repo_name, store_location=store_location)
286 oid=oid, repo=repo_name, store_location=store_location)
292 return store.has_oid()
287 return store.has_oid()
293
288
294 return False
289 return False
295
290
296 @reraise_safe_exceptions
291 @reraise_safe_exceptions
297 def store_path(self, wire, oid):
292 def store_path(self, wire, oid):
298 conf = self._wire_to_config(wire)
293 conf = self._wire_to_config(wire)
299 repo_init = self._factory.repo_libgit2(wire)
294 repo_init = self._factory.repo_libgit2(wire)
300 with repo_init as repo:
295 with repo_init as repo:
301 repo_name = repo.path
296 repo_name = repo.path
302
297
303 store_location = conf.get('vcs_git_lfs_store_location')
298 store_location = conf.get('vcs_git_lfs_store_location')
304 if store_location:
299 if store_location:
305 store = LFSOidStore(
300 store = LFSOidStore(
306 oid=oid, repo=repo_name, store_location=store_location)
301 oid=oid, repo=repo_name, store_location=store_location)
307 return store.oid_path
302 return store.oid_path
308 raise ValueError('Unable to fetch oid with path {}'.format(oid))
303 raise ValueError('Unable to fetch oid with path {}'.format(oid))
309
304
310 @reraise_safe_exceptions
305 @reraise_safe_exceptions
311 def bulk_request(self, wire, rev, pre_load):
306 def bulk_request(self, wire, rev, pre_load):
312 cache_on, context_uid, repo_id = self._cache_on(wire)
307 cache_on, context_uid, repo_id = self._cache_on(wire)
313 region = self._region(wire)
308 region = self._region(wire)
309
314 @region.conditional_cache_on_arguments(condition=cache_on)
310 @region.conditional_cache_on_arguments(condition=cache_on)
315 def _bulk_request(_repo_id, _rev, _pre_load):
311 def _bulk_request(_repo_id, _rev, _pre_load):
316 result = {}
312 result = {}
317 for attr in pre_load:
313 for attr in pre_load:
318 try:
314 try:
319 method = self._bulk_methods[attr]
315 method = self._bulk_methods[attr]
320 args = [wire, rev]
316 args = [wire, rev]
321 result[attr] = method(*args)
317 result[attr] = method(*args)
322 except KeyError as e:
318 except KeyError as e:
323 raise exceptions.VcsException(e)(
319 raise exceptions.VcsException(e)(
324 "Unknown bulk attribute: %s" % attr)
320 "Unknown bulk attribute: %s" % attr)
325 return result
321 return result
326
322
327 return _bulk_request(repo_id, rev, sorted(pre_load))
323 return _bulk_request(repo_id, rev, sorted(pre_load))
328
324
329 def _build_opener(self, url):
325 def _build_opener(self, url):
330 handlers = []
326 handlers = []
331 url_obj = url_parser(url)
327 url_obj = url_parser(url)
332 _, authinfo = url_obj.authinfo()
328 _, authinfo = url_obj.authinfo()
333
329
334 if authinfo:
330 if authinfo:
335 # create a password manager
331 # create a password manager
336 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
332 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
337 passmgr.add_password(*authinfo)
333 passmgr.add_password(*authinfo)
338
334
339 handlers.extend((httpbasicauthhandler(passmgr),
335 handlers.extend((httpbasicauthhandler(passmgr),
340 httpdigestauthhandler(passmgr)))
336 httpdigestauthhandler(passmgr)))
341
337
342 return urllib.request.build_opener(*handlers)
338 return urllib.request.build_opener(*handlers)
343
339
344 def _type_id_to_name(self, type_id):
340 def _type_id_to_name(self, type_id: int):
345 return {
341 return {
346 1: b'commit',
342 1: 'commit',
347 2: b'tree',
343 2: 'tree',
348 3: b'blob',
344 3: 'blob',
349 4: b'tag'
345 4: 'tag'
350 }[type_id]
346 }[type_id]
351
347
352 @reraise_safe_exceptions
348 @reraise_safe_exceptions
353 def check_url(self, url, config):
349 def check_url(self, url, config):
354 url_obj = url_parser(url)
350 url_obj = url_parser(url)
355 test_uri, _ = url_obj.authinfo()
351 test_uri, _ = url_obj.authinfo()
356 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
352 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
357 url_obj.query = obfuscate_qs(url_obj.query)
353 url_obj.query = obfuscate_qs(url_obj.query)
358 cleaned_uri = str(url_obj)
354 cleaned_uri = str(url_obj)
359 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
355 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
360
356
361 if not test_uri.endswith('info/refs'):
357 if not test_uri.endswith('info/refs'):
362 test_uri = test_uri.rstrip('/') + '/info/refs'
358 test_uri = test_uri.rstrip('/') + '/info/refs'
363
359
364 o = self._build_opener(url)
360 o = self._build_opener(url)
365 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
361 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
366
362
367 q = {"service": 'git-upload-pack'}
363 q = {"service": 'git-upload-pack'}
368 qs = '?%s' % urllib.parse.urlencode(q)
364 qs = '?%s' % urllib.parse.urlencode(q)
369 cu = "%s%s" % (test_uri, qs)
365 cu = "%s%s" % (test_uri, qs)
370 req = urllib.request.Request(cu, None, {})
366 req = urllib.request.Request(cu, None, {})
371
367
372 try:
368 try:
373 log.debug("Trying to open URL %s", cleaned_uri)
369 log.debug("Trying to open URL %s", cleaned_uri)
374 resp = o.open(req)
370 resp = o.open(req)
375 if resp.code != 200:
371 if resp.code != 200:
376 raise exceptions.URLError()('Return Code is not 200')
372 raise exceptions.URLError()('Return Code is not 200')
377 except Exception as e:
373 except Exception as e:
378 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
374 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
379 # means it cannot be cloned
375 # means it cannot be cloned
380 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
376 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
381
377
382 # now detect if it's proper git repo
378 # now detect if it's proper git repo
383 gitdata = resp.read()
379 gitdata = resp.read()
384 if 'service=git-upload-pack' in gitdata:
380 if 'service=git-upload-pack' in gitdata:
385 pass
381 pass
386 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
382 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
387 # old style git can return some other format !
383 # old style git can return some other format !
388 pass
384 pass
389 else:
385 else:
390 raise exceptions.URLError()(
386 raise exceptions.URLError()(
391 "url [%s] does not look like an git" % (cleaned_uri,))
387 "url [%s] does not look like an git" % (cleaned_uri,))
392
388
393 return True
389 return True
394
390
395 @reraise_safe_exceptions
391 @reraise_safe_exceptions
396 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
392 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
397 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
393 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
398 remote_refs = self.pull(wire, url, apply_refs=False)
394 remote_refs = self.pull(wire, url, apply_refs=False)
399 repo = self._factory.repo(wire)
395 repo = self._factory.repo(wire)
400 if isinstance(valid_refs, list):
396 if isinstance(valid_refs, list):
401 valid_refs = tuple(valid_refs)
397 valid_refs = tuple(valid_refs)
402
398
403 for k in remote_refs:
399 for k in remote_refs:
404 # only parse heads/tags and skip so called deferred tags
400 # only parse heads/tags and skip so called deferred tags
405 if k.startswith(valid_refs) and not k.endswith(deferred):
401 if k.startswith(valid_refs) and not k.endswith(deferred):
406 repo[k] = remote_refs[k]
402 repo[k] = remote_refs[k]
407
403
408 if update_after_clone:
404 if update_after_clone:
409 # we want to checkout HEAD
405 # we want to checkout HEAD
410 repo["HEAD"] = remote_refs["HEAD"]
406 repo["HEAD"] = remote_refs["HEAD"]
411 index.build_index_from_tree(repo.path, repo.index_path(),
407 index.build_index_from_tree(repo.path, repo.index_path(),
412 repo.object_store, repo["HEAD"].tree)
408 repo.object_store, repo["HEAD"].tree)
413
409
414 @reraise_safe_exceptions
410 @reraise_safe_exceptions
415 def branch(self, wire, commit_id):
411 def branch(self, wire, commit_id):
416 cache_on, context_uid, repo_id = self._cache_on(wire)
412 cache_on, context_uid, repo_id = self._cache_on(wire)
417 region = self._region(wire)
413 region = self._region(wire)
418 @region.conditional_cache_on_arguments(condition=cache_on)
414 @region.conditional_cache_on_arguments(condition=cache_on)
419 def _branch(_context_uid, _repo_id, _commit_id):
415 def _branch(_context_uid, _repo_id, _commit_id):
420 regex = re.compile('^refs/heads')
416 regex = re.compile('^refs/heads')
421
417
422 def filter_with(ref):
418 def filter_with(ref):
423 return regex.match(ref[0]) and ref[1] == _commit_id
419 return regex.match(ref[0]) and ref[1] == _commit_id
424
420
425 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
421 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
426 return [x[0].split('refs/heads/')[-1] for x in branches]
422 return [x[0].split('refs/heads/')[-1] for x in branches]
427
423
428 return _branch(context_uid, repo_id, commit_id)
424 return _branch(context_uid, repo_id, commit_id)
429
425
430 @reraise_safe_exceptions
426 @reraise_safe_exceptions
431 def commit_branches(self, wire, commit_id):
427 def commit_branches(self, wire, commit_id):
432 cache_on, context_uid, repo_id = self._cache_on(wire)
428 cache_on, context_uid, repo_id = self._cache_on(wire)
433 region = self._region(wire)
429 region = self._region(wire)
434 @region.conditional_cache_on_arguments(condition=cache_on)
430 @region.conditional_cache_on_arguments(condition=cache_on)
435 def _commit_branches(_context_uid, _repo_id, _commit_id):
431 def _commit_branches(_context_uid, _repo_id, _commit_id):
436 repo_init = self._factory.repo_libgit2(wire)
432 repo_init = self._factory.repo_libgit2(wire)
437 with repo_init as repo:
433 with repo_init as repo:
438 branches = [x for x in repo.branches.with_commit(_commit_id)]
434 branches = [x for x in repo.branches.with_commit(_commit_id)]
439 return branches
435 return branches
440
436
441 return _commit_branches(context_uid, repo_id, commit_id)
437 return _commit_branches(context_uid, repo_id, commit_id)
442
438
443 @reraise_safe_exceptions
439 @reraise_safe_exceptions
444 def add_object(self, wire, content):
440 def add_object(self, wire, content):
445 repo_init = self._factory.repo_libgit2(wire)
441 repo_init = self._factory.repo_libgit2(wire)
446 with repo_init as repo:
442 with repo_init as repo:
447 blob = objects.Blob()
443 blob = objects.Blob()
448 blob.set_raw_string(content)
444 blob.set_raw_string(content)
449 repo.object_store.add_object(blob)
445 repo.object_store.add_object(blob)
450 return blob.id
446 return blob.id
451
447
452 # TODO: this is quite complex, check if that can be simplified
448 # TODO: this is quite complex, check if that can be simplified
453 @reraise_safe_exceptions
449 @reraise_safe_exceptions
454 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
450 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
455 # Defines the root tree
451 # Defines the root tree
456 class _Root(object):
452 class _Root(object):
457 def __repr__(self):
453 def __repr__(self):
458 return 'ROOT TREE'
454 return 'ROOT TREE'
459 ROOT = _Root()
455 ROOT = _Root()
460
456
461 repo = self._factory.repo(wire)
457 repo = self._factory.repo(wire)
462 object_store = repo.object_store
458 object_store = repo.object_store
463
459
464 # Create tree and populates it with blobs
460 # Create tree and populates it with blobs
465
461
466 if commit_tree and repo[commit_tree]:
462 if commit_tree and repo[commit_tree]:
467 git_commit = repo[commit_data['parents'][0]]
463 git_commit = repo[commit_data['parents'][0]]
468 commit_tree = repo[git_commit.tree] # root tree
464 commit_tree = repo[git_commit.tree] # root tree
469 else:
465 else:
470 commit_tree = objects.Tree()
466 commit_tree = objects.Tree()
471
467
472 for node in updated:
468 for node in updated:
473 # Compute subdirs if needed
469 # Compute subdirs if needed
474 dirpath, nodename = vcspath.split(node['path'])
470 dirpath, nodename = vcspath.split(node['path'])
475 dirnames = list(map(safe_str, dirpath and dirpath.split('/') or []))
471 dirnames = list(map(safe_str, dirpath and dirpath.split('/') or []))
476 parent = commit_tree
472 parent = commit_tree
477 ancestors = [('', parent)]
473 ancestors = [('', parent)]
478
474
479 # Tries to dig for the deepest existing tree
475 # Tries to dig for the deepest existing tree
480 while dirnames:
476 while dirnames:
481 curdir = dirnames.pop(0)
477 curdir = dirnames.pop(0)
482 try:
478 try:
483 dir_id = parent[curdir][1]
479 dir_id = parent[curdir][1]
484 except KeyError:
480 except KeyError:
485 # put curdir back into dirnames and stops
481 # put curdir back into dirnames and stops
486 dirnames.insert(0, curdir)
482 dirnames.insert(0, curdir)
487 break
483 break
488 else:
484 else:
489 # If found, updates parent
485 # If found, updates parent
490 parent = repo[dir_id]
486 parent = repo[dir_id]
491 ancestors.append((curdir, parent))
487 ancestors.append((curdir, parent))
492 # Now parent is deepest existing tree and we need to create
488 # Now parent is deepest existing tree and we need to create
493 # subtrees for dirnames (in reverse order)
489 # subtrees for dirnames (in reverse order)
494 # [this only applies for nodes from added]
490 # [this only applies for nodes from added]
495 new_trees = []
491 new_trees = []
496
492
497 blob = objects.Blob.from_string(node['content'])
493 blob = objects.Blob.from_string(node['content'])
498
494
499 if dirnames:
495 if dirnames:
500 # If there are trees which should be created we need to build
496 # If there are trees which should be created we need to build
501 # them now (in reverse order)
497 # them now (in reverse order)
502 reversed_dirnames = list(reversed(dirnames))
498 reversed_dirnames = list(reversed(dirnames))
503 curtree = objects.Tree()
499 curtree = objects.Tree()
504 curtree[node['node_path']] = node['mode'], blob.id
500 curtree[node['node_path']] = node['mode'], blob.id
505 new_trees.append(curtree)
501 new_trees.append(curtree)
506 for dirname in reversed_dirnames[:-1]:
502 for dirname in reversed_dirnames[:-1]:
507 newtree = objects.Tree()
503 newtree = objects.Tree()
508 newtree[dirname] = (DIR_STAT, curtree.id)
504 newtree[dirname] = (DIR_STAT, curtree.id)
509 new_trees.append(newtree)
505 new_trees.append(newtree)
510 curtree = newtree
506 curtree = newtree
511 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
507 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
512 else:
508 else:
513 parent.add(name=node['node_path'], mode=node['mode'], hexsha=blob.id)
509 parent.add(name=node['node_path'], mode=node['mode'], hexsha=blob.id)
514
510
515 new_trees.append(parent)
511 new_trees.append(parent)
516 # Update ancestors
512 # Update ancestors
517 reversed_ancestors = reversed(
513 reversed_ancestors = reversed(
518 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
514 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
519 for parent, tree, path in reversed_ancestors:
515 for parent, tree, path in reversed_ancestors:
520 parent[path] = (DIR_STAT, tree.id)
516 parent[path] = (DIR_STAT, tree.id)
521 object_store.add_object(tree)
517 object_store.add_object(tree)
522
518
523 object_store.add_object(blob)
519 object_store.add_object(blob)
524 for tree in new_trees:
520 for tree in new_trees:
525 object_store.add_object(tree)
521 object_store.add_object(tree)
526
522
527 for node_path in removed:
523 for node_path in removed:
528 paths = node_path.split('/')
524 paths = node_path.split('/')
529 tree = commit_tree # start with top-level
525 tree = commit_tree # start with top-level
530 trees = [{'tree': tree, 'path': ROOT}]
526 trees = [{'tree': tree, 'path': ROOT}]
531 # Traverse deep into the forest...
527 # Traverse deep into the forest...
532 # resolve final tree by iterating the path.
528 # resolve final tree by iterating the path.
533 # e.g a/b/c.txt will get
529 # e.g a/b/c.txt will get
534 # - root as tree then
530 # - root as tree then
535 # - 'a' as tree,
531 # - 'a' as tree,
536 # - 'b' as tree,
532 # - 'b' as tree,
537 # - stop at c as blob.
533 # - stop at c as blob.
538 for path in paths:
534 for path in paths:
539 try:
535 try:
540 obj = repo[tree[path][1]]
536 obj = repo[tree[path][1]]
541 if isinstance(obj, objects.Tree):
537 if isinstance(obj, objects.Tree):
542 trees.append({'tree': obj, 'path': path})
538 trees.append({'tree': obj, 'path': path})
543 tree = obj
539 tree = obj
544 except KeyError:
540 except KeyError:
545 break
541 break
546 #PROBLEM:
542 #PROBLEM:
547 """
543 """
548 We're not editing same reference tree object
544 We're not editing same reference tree object
549 """
545 """
550 # Cut down the blob and all rotten trees on the way back...
546 # Cut down the blob and all rotten trees on the way back...
551 for path, tree_data in reversed(list(zip(paths, trees))):
547 for path, tree_data in reversed(list(zip(paths, trees))):
552 tree = tree_data['tree']
548 tree = tree_data['tree']
553 tree.__delitem__(path)
549 tree.__delitem__(path)
554 # This operation edits the tree, we need to mark new commit back
550 # This operation edits the tree, we need to mark new commit back
555
551
556 if len(tree) > 0:
552 if len(tree) > 0:
557 # This tree still has elements - don't remove it or any
553 # This tree still has elements - don't remove it or any
558 # of it's parents
554 # of it's parents
559 break
555 break
560
556
561 object_store.add_object(commit_tree)
557 object_store.add_object(commit_tree)
562
558
563 # Create commit
559 # Create commit
564 commit = objects.Commit()
560 commit = objects.Commit()
565 commit.tree = commit_tree.id
561 commit.tree = commit_tree.id
566 for k, v in commit_data.items():
562 for k, v in commit_data.items():
567 setattr(commit, k, v)
563 setattr(commit, k, v)
568 object_store.add_object(commit)
564 object_store.add_object(commit)
569
565
570 self.create_branch(wire, branch, commit.id)
566 self.create_branch(wire, branch, commit.id)
571
567
572 # dulwich set-ref
568 # dulwich set-ref
573 ref = 'refs/heads/%s' % branch
569 ref = 'refs/heads/%s' % branch
574 repo.refs[ref] = commit.id
570 repo.refs[ref] = commit.id
575
571
576 return commit.id
572 return commit.id
577
573
578 @reraise_safe_exceptions
574 @reraise_safe_exceptions
579 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
575 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
580 if url != 'default' and '://' not in url:
576 if url != 'default' and '://' not in url:
581 client = LocalGitClient(url)
577 client = LocalGitClient(url)
582 else:
578 else:
583 url_obj = url_parser(url)
579 url_obj = url_parser(url)
584 o = self._build_opener(url)
580 o = self._build_opener(url)
585 url, _ = url_obj.authinfo()
581 url, _ = url_obj.authinfo()
586 client = HttpGitClient(base_url=url, opener=o)
582 client = HttpGitClient(base_url=url, opener=o)
587 repo = self._factory.repo(wire)
583 repo = self._factory.repo(wire)
588
584
589 determine_wants = repo.object_store.determine_wants_all
585 determine_wants = repo.object_store.determine_wants_all
590 if refs:
586 if refs:
591 def determine_wants_requested(references):
587 def determine_wants_requested(references):
592 return [references[r] for r in references if r in refs]
588 return [references[r] for r in references if r in refs]
593 determine_wants = determine_wants_requested
589 determine_wants = determine_wants_requested
594
590
595 try:
591 try:
596 remote_refs = client.fetch(
592 remote_refs = client.fetch(
597 path=url, target=repo, determine_wants=determine_wants)
593 path=url, target=repo, determine_wants=determine_wants)
598 except NotGitRepository as e:
594 except NotGitRepository as e:
599 log.warning(
595 log.warning(
600 'Trying to fetch from "%s" failed, not a Git repository.', url)
596 'Trying to fetch from "%s" failed, not a Git repository.', url)
601 # Exception can contain unicode which we convert
597 # Exception can contain unicode which we convert
602 raise exceptions.AbortException(e)(repr(e))
598 raise exceptions.AbortException(e)(repr(e))
603
599
604 # mikhail: client.fetch() returns all the remote refs, but fetches only
600 # mikhail: client.fetch() returns all the remote refs, but fetches only
605 # refs filtered by `determine_wants` function. We need to filter result
601 # refs filtered by `determine_wants` function. We need to filter result
606 # as well
602 # as well
607 if refs:
603 if refs:
608 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
604 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
609
605
610 if apply_refs:
606 if apply_refs:
611 # TODO: johbo: Needs proper test coverage with a git repository
607 # TODO: johbo: Needs proper test coverage with a git repository
612 # that contains a tag object, so that we would end up with
608 # that contains a tag object, so that we would end up with
613 # a peeled ref at this point.
609 # a peeled ref at this point.
614 for k in remote_refs:
610 for k in remote_refs:
615 if k.endswith(PEELED_REF_MARKER):
611 if k.endswith(PEELED_REF_MARKER):
616 log.debug("Skipping peeled reference %s", k)
612 log.debug("Skipping peeled reference %s", k)
617 continue
613 continue
618 repo[k] = remote_refs[k]
614 repo[k] = remote_refs[k]
619
615
620 if refs and not update_after:
616 if refs and not update_after:
621 # mikhail: explicitly set the head to the last ref.
617 # mikhail: explicitly set the head to the last ref.
622 repo["HEAD"] = remote_refs[refs[-1]]
618 repo["HEAD"] = remote_refs[refs[-1]]
623
619
624 if update_after:
620 if update_after:
625 # we want to checkout HEAD
621 # we want to checkout HEAD
626 repo["HEAD"] = remote_refs["HEAD"]
622 repo["HEAD"] = remote_refs["HEAD"]
627 index.build_index_from_tree(repo.path, repo.index_path(),
623 index.build_index_from_tree(repo.path, repo.index_path(),
628 repo.object_store, repo["HEAD"].tree)
624 repo.object_store, repo["HEAD"].tree)
629 return remote_refs
625 return remote_refs
630
626
631 @reraise_safe_exceptions
627 @reraise_safe_exceptions
632 def sync_fetch(self, wire, url, refs=None, all_refs=False):
628 def sync_fetch(self, wire, url, refs=None, all_refs=False):
633 repo = self._factory.repo(wire)
629 repo = self._factory.repo(wire)
634 if refs and not isinstance(refs, (list, tuple)):
630 if refs and not isinstance(refs, (list, tuple)):
635 refs = [refs]
631 refs = [refs]
636
632
637 config = self._wire_to_config(wire)
633 config = self._wire_to_config(wire)
638 # get all remote refs we'll use to fetch later
634 # get all remote refs we'll use to fetch later
639 cmd = ['ls-remote']
635 cmd = ['ls-remote']
640 if not all_refs:
636 if not all_refs:
641 cmd += ['--heads', '--tags']
637 cmd += ['--heads', '--tags']
642 cmd += [url]
638 cmd += [url]
643 output, __ = self.run_git_command(
639 output, __ = self.run_git_command(
644 wire, cmd, fail_on_stderr=False,
640 wire, cmd, fail_on_stderr=False,
645 _copts=self._remote_conf(config),
641 _copts=self._remote_conf(config),
646 extra_env={'GIT_TERMINAL_PROMPT': '0'})
642 extra_env={'GIT_TERMINAL_PROMPT': '0'})
647
643
648 remote_refs = collections.OrderedDict()
644 remote_refs = collections.OrderedDict()
649 fetch_refs = []
645 fetch_refs = []
650
646
651 for ref_line in output.splitlines():
647 for ref_line in output.splitlines():
652 sha, ref = ref_line.split('\t')
648 sha, ref = ref_line.split('\t')
653 sha = sha.strip()
649 sha = sha.strip()
654 if ref in remote_refs:
650 if ref in remote_refs:
655 # duplicate, skip
651 # duplicate, skip
656 continue
652 continue
657 if ref.endswith(PEELED_REF_MARKER):
653 if ref.endswith(PEELED_REF_MARKER):
658 log.debug("Skipping peeled reference %s", ref)
654 log.debug("Skipping peeled reference %s", ref)
659 continue
655 continue
660 # don't sync HEAD
656 # don't sync HEAD
661 if ref in ['HEAD']:
657 if ref in ['HEAD']:
662 continue
658 continue
663
659
664 remote_refs[ref] = sha
660 remote_refs[ref] = sha
665
661
666 if refs and sha in refs:
662 if refs and sha in refs:
667 # we filter fetch using our specified refs
663 # we filter fetch using our specified refs
668 fetch_refs.append('{}:{}'.format(ref, ref))
664 fetch_refs.append('{}:{}'.format(ref, ref))
669 elif not refs:
665 elif not refs:
670 fetch_refs.append('{}:{}'.format(ref, ref))
666 fetch_refs.append('{}:{}'.format(ref, ref))
671 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
667 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
672
668
673 if fetch_refs:
669 if fetch_refs:
674 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
670 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
675 fetch_refs_chunks = list(chunk)
671 fetch_refs_chunks = list(chunk)
676 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
672 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
677 _out, _err = self.run_git_command(
673 self.run_git_command(
678 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
674 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
679 fail_on_stderr=False,
675 fail_on_stderr=False,
680 _copts=self._remote_conf(config),
676 _copts=self._remote_conf(config),
681 extra_env={'GIT_TERMINAL_PROMPT': '0'})
677 extra_env={'GIT_TERMINAL_PROMPT': '0'})
682
678
683 return remote_refs
679 return remote_refs
684
680
685 @reraise_safe_exceptions
681 @reraise_safe_exceptions
686 def sync_push(self, wire, url, refs=None):
682 def sync_push(self, wire, url, refs=None):
687 if not self.check_url(url, wire):
683 if not self.check_url(url, wire):
688 return
684 return
689 config = self._wire_to_config(wire)
685 config = self._wire_to_config(wire)
690 self._factory.repo(wire)
686 self._factory.repo(wire)
691 self.run_git_command(
687 self.run_git_command(
692 wire, ['push', url, '--mirror'], fail_on_stderr=False,
688 wire, ['push', url, '--mirror'], fail_on_stderr=False,
693 _copts=self._remote_conf(config),
689 _copts=self._remote_conf(config),
694 extra_env={'GIT_TERMINAL_PROMPT': '0'})
690 extra_env={'GIT_TERMINAL_PROMPT': '0'})
695
691
696 @reraise_safe_exceptions
692 @reraise_safe_exceptions
697 def get_remote_refs(self, wire, url):
693 def get_remote_refs(self, wire, url):
698 repo = Repo(url)
694 repo = Repo(url)
699 return repo.get_refs()
695 return repo.get_refs()
700
696
701 @reraise_safe_exceptions
697 @reraise_safe_exceptions
702 def get_description(self, wire):
698 def get_description(self, wire):
703 repo = self._factory.repo(wire)
699 repo = self._factory.repo(wire)
704 return repo.get_description()
700 return repo.get_description()
705
701
706 @reraise_safe_exceptions
702 @reraise_safe_exceptions
707 def get_missing_revs(self, wire, rev1, rev2, path2):
703 def get_missing_revs(self, wire, rev1, rev2, path2):
708 repo = self._factory.repo(wire)
704 repo = self._factory.repo(wire)
709 LocalGitClient(thin_packs=False).fetch(path2, repo)
705 LocalGitClient(thin_packs=False).fetch(path2, repo)
710
706
711 wire_remote = wire.copy()
707 wire_remote = wire.copy()
712 wire_remote['path'] = path2
708 wire_remote['path'] = path2
713 repo_remote = self._factory.repo(wire_remote)
709 repo_remote = self._factory.repo(wire_remote)
714 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
710 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
715
711
716 revs = [
712 revs = [
717 x.commit.id
713 x.commit.id
718 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
714 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
719 return revs
715 return revs
720
716
721 @reraise_safe_exceptions
717 @reraise_safe_exceptions
722 def get_object(self, wire, sha, maybe_unreachable=False):
718 def get_object(self, wire, sha, maybe_unreachable=False):
723 cache_on, context_uid, repo_id = self._cache_on(wire)
719 cache_on, context_uid, repo_id = self._cache_on(wire)
724 region = self._region(wire)
720 region = self._region(wire)
721
725 @region.conditional_cache_on_arguments(condition=cache_on)
722 @region.conditional_cache_on_arguments(condition=cache_on)
726 def _get_object(_context_uid, _repo_id, _sha):
723 def _get_object(_context_uid, _repo_id, _sha):
727 repo_init = self._factory.repo_libgit2(wire)
724 repo_init = self._factory.repo_libgit2(wire)
728 with repo_init as repo:
725 with repo_init as repo:
729
726
730 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
727 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
731 try:
728 try:
732 commit = repo.revparse_single(sha)
729 commit = repo.revparse_single(sha)
733 except KeyError:
730 except KeyError:
734 # NOTE(marcink): KeyError doesn't give us any meaningful information
731 # NOTE(marcink): KeyError doesn't give us any meaningful information
735 # here, we instead give something more explicit
732 # here, we instead give something more explicit
736 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
733 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
737 raise exceptions.LookupException(e)(missing_commit_err)
734 raise exceptions.LookupException(e)(missing_commit_err)
738 except ValueError as e:
735 except ValueError as e:
739 raise exceptions.LookupException(e)(missing_commit_err)
736 raise exceptions.LookupException(e)(missing_commit_err)
740
737
741 is_tag = False
738 is_tag = False
742 if isinstance(commit, pygit2.Tag):
739 if isinstance(commit, pygit2.Tag):
743 commit = repo.get(commit.target)
740 commit = repo.get(commit.target)
744 is_tag = True
741 is_tag = True
745
742
746 check_dangling = True
743 check_dangling = True
747 if is_tag:
744 if is_tag:
748 check_dangling = False
745 check_dangling = False
749
746
750 if check_dangling and maybe_unreachable:
747 if check_dangling and maybe_unreachable:
751 check_dangling = False
748 check_dangling = False
752
749
753 # we used a reference and it parsed means we're not having a dangling commit
750 # we used a reference and it parsed means we're not having a dangling commit
754 if sha != commit.hex:
751 if sha != commit.hex:
755 check_dangling = False
752 check_dangling = False
756
753
757 if check_dangling:
754 if check_dangling:
758 # check for dangling commit
755 # check for dangling commit
759 for branch in repo.branches.with_commit(commit.hex):
756 for branch in repo.branches.with_commit(commit.hex):
760 if branch:
757 if branch:
761 break
758 break
762 else:
759 else:
763 # NOTE(marcink): Empty error doesn't give us any meaningful information
760 # NOTE(marcink): Empty error doesn't give us any meaningful information
764 # here, we instead give something more explicit
761 # here, we instead give something more explicit
765 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
762 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
766 raise exceptions.LookupException(e)(missing_commit_err)
763 raise exceptions.LookupException(e)(missing_commit_err)
767
764
768 commit_id = commit.hex
765 commit_id = commit.hex
769 type_id = commit.type_str
766 type_id = commit.type
770
767
771 return {
768 return {
772 'id': commit_id,
769 'id': commit_id,
773 'type': self._type_id_to_name(type_id),
770 'type': self._type_id_to_name(type_id),
774 'commit_id': commit_id,
771 'commit_id': commit_id,
775 'idx': 0
772 'idx': 0
776 }
773 }
777
774
778 return _get_object(context_uid, repo_id, sha)
775 return _get_object(context_uid, repo_id, sha)
779
776
780 @reraise_safe_exceptions
777 @reraise_safe_exceptions
781 def get_refs(self, wire):
778 def get_refs(self, wire):
782 cache_on, context_uid, repo_id = self._cache_on(wire)
779 cache_on, context_uid, repo_id = self._cache_on(wire)
783 region = self._region(wire)
780 region = self._region(wire)
781
784 @region.conditional_cache_on_arguments(condition=cache_on)
782 @region.conditional_cache_on_arguments(condition=cache_on)
785 def _get_refs(_context_uid, _repo_id):
783 def _get_refs(_context_uid, _repo_id):
786
784
787 repo_init = self._factory.repo_libgit2(wire)
785 repo_init = self._factory.repo_libgit2(wire)
788 with repo_init as repo:
786 with repo_init as repo:
789 regex = re.compile('^refs/(heads|tags)/')
787 regex = re.compile('^refs/(heads|tags)/')
790 return {x.name: x.target.hex for x in
788 return {x.name: x.target.hex for x in
791 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
789 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
792
790
793 return _get_refs(context_uid, repo_id)
791 return _get_refs(context_uid, repo_id)
794
792
795 @reraise_safe_exceptions
793 @reraise_safe_exceptions
796 def get_branch_pointers(self, wire):
794 def get_branch_pointers(self, wire):
797 cache_on, context_uid, repo_id = self._cache_on(wire)
795 cache_on, context_uid, repo_id = self._cache_on(wire)
798 region = self._region(wire)
796 region = self._region(wire)
797
799 @region.conditional_cache_on_arguments(condition=cache_on)
798 @region.conditional_cache_on_arguments(condition=cache_on)
800 def _get_branch_pointers(_context_uid, _repo_id):
799 def _get_branch_pointers(_context_uid, _repo_id):
801
800
802 repo_init = self._factory.repo_libgit2(wire)
801 repo_init = self._factory.repo_libgit2(wire)
803 regex = re.compile('^refs/heads')
802 regex = re.compile('^refs/heads')
804 with repo_init as repo:
803 with repo_init as repo:
805 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
804 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
806 return {x.target.hex: x.shorthand for x in branches}
805 return {x.target.hex: x.shorthand for x in branches}
807
806
808 return _get_branch_pointers(context_uid, repo_id)
807 return _get_branch_pointers(context_uid, repo_id)
809
808
810 @reraise_safe_exceptions
809 @reraise_safe_exceptions
811 def head(self, wire, show_exc=True):
810 def head(self, wire, show_exc=True):
812 cache_on, context_uid, repo_id = self._cache_on(wire)
811 cache_on, context_uid, repo_id = self._cache_on(wire)
813 region = self._region(wire)
812 region = self._region(wire)
813
814 @region.conditional_cache_on_arguments(condition=cache_on)
814 @region.conditional_cache_on_arguments(condition=cache_on)
815 def _head(_context_uid, _repo_id, _show_exc):
815 def _head(_context_uid, _repo_id, _show_exc):
816 repo_init = self._factory.repo_libgit2(wire)
816 repo_init = self._factory.repo_libgit2(wire)
817 with repo_init as repo:
817 with repo_init as repo:
818 try:
818 try:
819 return repo.head.peel().hex
819 return repo.head.peel().hex
820 except Exception:
820 except Exception:
821 if show_exc:
821 if show_exc:
822 raise
822 raise
823 return _head(context_uid, repo_id, show_exc)
823 return _head(context_uid, repo_id, show_exc)
824
824
825 @reraise_safe_exceptions
825 @reraise_safe_exceptions
826 def init(self, wire):
826 def init(self, wire):
827 repo_path = str_to_dulwich(wire['path'])
827 repo_path = str_to_dulwich(wire['path'])
828 self.repo = Repo.init(repo_path)
828 self.repo = Repo.init(repo_path)
829
829
830 @reraise_safe_exceptions
830 @reraise_safe_exceptions
831 def init_bare(self, wire):
831 def init_bare(self, wire):
832 repo_path = str_to_dulwich(wire['path'])
832 repo_path = str_to_dulwich(wire['path'])
833 self.repo = Repo.init_bare(repo_path)
833 self.repo = Repo.init_bare(repo_path)
834
834
835 @reraise_safe_exceptions
835 @reraise_safe_exceptions
836 def revision(self, wire, rev):
836 def revision(self, wire, rev):
837
837
838 cache_on, context_uid, repo_id = self._cache_on(wire)
838 cache_on, context_uid, repo_id = self._cache_on(wire)
839 region = self._region(wire)
839 region = self._region(wire)
840
840 @region.conditional_cache_on_arguments(condition=cache_on)
841 @region.conditional_cache_on_arguments(condition=cache_on)
841 def _revision(_context_uid, _repo_id, _rev):
842 def _revision(_context_uid, _repo_id, _rev):
842 repo_init = self._factory.repo_libgit2(wire)
843 repo_init = self._factory.repo_libgit2(wire)
843 with repo_init as repo:
844 with repo_init as repo:
844 commit = repo[rev]
845 commit = repo[rev]
845 obj_data = {
846 obj_data = {
846 'id': commit.id.hex,
847 'id': commit.id.hex,
847 }
848 }
848 # tree objects itself don't have tree_id attribute
849 # tree objects itself don't have tree_id attribute
849 if hasattr(commit, 'tree_id'):
850 if hasattr(commit, 'tree_id'):
850 obj_data['tree'] = commit.tree_id.hex
851 obj_data['tree'] = commit.tree_id.hex
851
852
852 return obj_data
853 return obj_data
853 return _revision(context_uid, repo_id, rev)
854 return _revision(context_uid, repo_id, rev)
854
855
855 @reraise_safe_exceptions
856 @reraise_safe_exceptions
856 def date(self, wire, commit_id):
857 def date(self, wire, commit_id):
857 cache_on, context_uid, repo_id = self._cache_on(wire)
858 cache_on, context_uid, repo_id = self._cache_on(wire)
858 region = self._region(wire)
859 region = self._region(wire)
860
859 @region.conditional_cache_on_arguments(condition=cache_on)
861 @region.conditional_cache_on_arguments(condition=cache_on)
860 def _date(_repo_id, _commit_id):
862 def _date(_repo_id, _commit_id):
861 repo_init = self._factory.repo_libgit2(wire)
863 repo_init = self._factory.repo_libgit2(wire)
862 with repo_init as repo:
864 with repo_init as repo:
863 commit = repo[commit_id]
865 commit = repo[commit_id]
864
866
865 if hasattr(commit, 'commit_time'):
867 if hasattr(commit, 'commit_time'):
866 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
868 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
867 else:
869 else:
868 commit = commit.get_object()
870 commit = commit.get_object()
869 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
871 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
870
872
871 # TODO(marcink): check dulwich difference of offset vs timezone
873 # TODO(marcink): check dulwich difference of offset vs timezone
872 return [commit_time, commit_time_offset]
874 return [commit_time, commit_time_offset]
873 return _date(repo_id, commit_id)
875 return _date(repo_id, commit_id)
874
876
875 @reraise_safe_exceptions
877 @reraise_safe_exceptions
876 def author(self, wire, commit_id):
878 def author(self, wire, commit_id):
877 cache_on, context_uid, repo_id = self._cache_on(wire)
879 cache_on, context_uid, repo_id = self._cache_on(wire)
878 region = self._region(wire)
880 region = self._region(wire)
881
879 @region.conditional_cache_on_arguments(condition=cache_on)
882 @region.conditional_cache_on_arguments(condition=cache_on)
880 def _author(_repo_id, _commit_id):
883 def _author(_repo_id, _commit_id):
881 repo_init = self._factory.repo_libgit2(wire)
884 repo_init = self._factory.repo_libgit2(wire)
882 with repo_init as repo:
885 with repo_init as repo:
883 commit = repo[commit_id]
886 commit = repo[commit_id]
884
887
885 if hasattr(commit, 'author'):
888 if hasattr(commit, 'author'):
886 author = commit.author
889 author = commit.author
887 else:
890 else:
888 author = commit.get_object().author
891 author = commit.get_object().author
889
892
890 if author.email:
893 if author.email:
891 return "{} <{}>".format(author.name, author.email)
894 return "{} <{}>".format(author.name, author.email)
892
895
893 try:
896 try:
894 return "{}".format(author.name)
897 return "{}".format(author.name)
895 except Exception:
898 except Exception:
896 return "{}".format(safe_unicode(author.raw_name))
899 return "{}".format(safe_str(author.raw_name))
897
900
898 return _author(repo_id, commit_id)
901 return _author(repo_id, commit_id)
899
902
900 @reraise_safe_exceptions
903 @reraise_safe_exceptions
901 def message(self, wire, commit_id):
904 def message(self, wire, commit_id):
902 cache_on, context_uid, repo_id = self._cache_on(wire)
905 cache_on, context_uid, repo_id = self._cache_on(wire)
903 region = self._region(wire)
906 region = self._region(wire)
904 @region.conditional_cache_on_arguments(condition=cache_on)
907 @region.conditional_cache_on_arguments(condition=cache_on)
905 def _message(_repo_id, _commit_id):
908 def _message(_repo_id, _commit_id):
906 repo_init = self._factory.repo_libgit2(wire)
909 repo_init = self._factory.repo_libgit2(wire)
907 with repo_init as repo:
910 with repo_init as repo:
908 commit = repo[commit_id]
911 commit = repo[commit_id]
909 return commit.message
912 return commit.message
910 return _message(repo_id, commit_id)
913 return _message(repo_id, commit_id)
911
914
912 @reraise_safe_exceptions
915 @reraise_safe_exceptions
913 def parents(self, wire, commit_id):
916 def parents(self, wire, commit_id):
914 cache_on, context_uid, repo_id = self._cache_on(wire)
917 cache_on, context_uid, repo_id = self._cache_on(wire)
915 region = self._region(wire)
918 region = self._region(wire)
916 @region.conditional_cache_on_arguments(condition=cache_on)
919 @region.conditional_cache_on_arguments(condition=cache_on)
917 def _parents(_repo_id, _commit_id):
920 def _parents(_repo_id, _commit_id):
918 repo_init = self._factory.repo_libgit2(wire)
921 repo_init = self._factory.repo_libgit2(wire)
919 with repo_init as repo:
922 with repo_init as repo:
920 commit = repo[commit_id]
923 commit = repo[commit_id]
921 if hasattr(commit, 'parent_ids'):
924 if hasattr(commit, 'parent_ids'):
922 parent_ids = commit.parent_ids
925 parent_ids = commit.parent_ids
923 else:
926 else:
924 parent_ids = commit.get_object().parent_ids
927 parent_ids = commit.get_object().parent_ids
925
928
926 return [x.hex for x in parent_ids]
929 return [x.hex for x in parent_ids]
927 return _parents(repo_id, commit_id)
930 return _parents(repo_id, commit_id)
928
931
929 @reraise_safe_exceptions
932 @reraise_safe_exceptions
930 def children(self, wire, commit_id):
933 def children(self, wire, commit_id):
931 cache_on, context_uid, repo_id = self._cache_on(wire)
934 cache_on, context_uid, repo_id = self._cache_on(wire)
932 region = self._region(wire)
935 region = self._region(wire)
936
933 @region.conditional_cache_on_arguments(condition=cache_on)
937 @region.conditional_cache_on_arguments(condition=cache_on)
934 def _children(_repo_id, _commit_id):
938 def _children(_repo_id, _commit_id):
935 output, __ = self.run_git_command(
939 output, __ = self.run_git_command(
936 wire, ['rev-list', '--all', '--children'])
940 wire, ['rev-list', '--all', '--children'])
937
941
938 child_ids = []
942 child_ids = []
939 pat = re.compile(r'^%s' % commit_id)
943 pat = re.compile(r'^%s' % commit_id)
940 for l in output.splitlines():
944 for l in output.splitlines():
941 if pat.match(l):
945 if pat.match(l):
942 found_ids = l.split(' ')[1:]
946 found_ids = l.split(' ')[1:]
943 child_ids.extend(found_ids)
947 child_ids.extend(found_ids)
944
948
945 return child_ids
949 return child_ids
946 return _children(repo_id, commit_id)
950 return _children(repo_id, commit_id)
947
951
948 @reraise_safe_exceptions
952 @reraise_safe_exceptions
949 def set_refs(self, wire, key, value):
953 def set_refs(self, wire, key, value):
950 repo_init = self._factory.repo_libgit2(wire)
954 repo_init = self._factory.repo_libgit2(wire)
951 with repo_init as repo:
955 with repo_init as repo:
952 repo.references.create(key, value, force=True)
956 repo.references.create(key, value, force=True)
953
957
954 @reraise_safe_exceptions
958 @reraise_safe_exceptions
955 def create_branch(self, wire, branch_name, commit_id, force=False):
959 def create_branch(self, wire, branch_name, commit_id, force=False):
956 repo_init = self._factory.repo_libgit2(wire)
960 repo_init = self._factory.repo_libgit2(wire)
957 with repo_init as repo:
961 with repo_init as repo:
958 commit = repo[commit_id]
962 commit = repo[commit_id]
959
963
960 if force:
964 if force:
961 repo.branches.local.create(branch_name, commit, force=force)
965 repo.branches.local.create(branch_name, commit, force=force)
962 elif not repo.branches.get(branch_name):
966 elif not repo.branches.get(branch_name):
963 # create only if that branch isn't existing
967 # create only if that branch isn't existing
964 repo.branches.local.create(branch_name, commit, force=force)
968 repo.branches.local.create(branch_name, commit, force=force)
965
969
966 @reraise_safe_exceptions
970 @reraise_safe_exceptions
967 def remove_ref(self, wire, key):
971 def remove_ref(self, wire, key):
968 repo_init = self._factory.repo_libgit2(wire)
972 repo_init = self._factory.repo_libgit2(wire)
969 with repo_init as repo:
973 with repo_init as repo:
970 repo.references.delete(key)
974 repo.references.delete(key)
971
975
972 @reraise_safe_exceptions
976 @reraise_safe_exceptions
973 def tag_remove(self, wire, tag_name):
977 def tag_remove(self, wire, tag_name):
974 repo_init = self._factory.repo_libgit2(wire)
978 repo_init = self._factory.repo_libgit2(wire)
975 with repo_init as repo:
979 with repo_init as repo:
976 key = 'refs/tags/{}'.format(tag_name)
980 key = 'refs/tags/{}'.format(tag_name)
977 repo.references.delete(key)
981 repo.references.delete(key)
978
982
979 @reraise_safe_exceptions
983 @reraise_safe_exceptions
980 def tree_changes(self, wire, source_id, target_id):
984 def tree_changes(self, wire, source_id, target_id):
981 # TODO(marcink): remove this seems it's only used by tests
985 # TODO(marcink): remove this seems it's only used by tests
982 repo = self._factory.repo(wire)
986 repo = self._factory.repo(wire)
983 source = repo[source_id].tree if source_id else None
987 source = repo[source_id].tree if source_id else None
984 target = repo[target_id].tree
988 target = repo[target_id].tree
985 result = repo.object_store.tree_changes(source, target)
989 result = repo.object_store.tree_changes(source, target)
986 return list(result)
990 return list(result)
987
991
988 @reraise_safe_exceptions
992 @reraise_safe_exceptions
989 def tree_and_type_for_path(self, wire, commit_id, path):
993 def tree_and_type_for_path(self, wire, commit_id, path):
990
994
991 cache_on, context_uid, repo_id = self._cache_on(wire)
995 cache_on, context_uid, repo_id = self._cache_on(wire)
992 region = self._region(wire)
996 region = self._region(wire)
997
993 @region.conditional_cache_on_arguments(condition=cache_on)
998 @region.conditional_cache_on_arguments(condition=cache_on)
994 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
999 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
995 repo_init = self._factory.repo_libgit2(wire)
1000 repo_init = self._factory.repo_libgit2(wire)
996
1001
997 with repo_init as repo:
1002 with repo_init as repo:
998 commit = repo[commit_id]
1003 commit = repo[commit_id]
999 try:
1004 try:
1000 tree = commit.tree[path]
1005 tree = commit.tree[path]
1001 except KeyError:
1006 except KeyError:
1002 return None, None, None
1007 return None, None, None
1003
1008
1004 return tree.id.hex, tree.type_str, tree.filemode
1009 return tree.id.hex, tree.type_str, tree.filemode
1005 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1010 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1006
1011
1007 @reraise_safe_exceptions
1012 @reraise_safe_exceptions
1008 def tree_items(self, wire, tree_id):
1013 def tree_items(self, wire, tree_id):
1009 cache_on, context_uid, repo_id = self._cache_on(wire)
1014 cache_on, context_uid, repo_id = self._cache_on(wire)
1010 region = self._region(wire)
1015 region = self._region(wire)
1016
1011 @region.conditional_cache_on_arguments(condition=cache_on)
1017 @region.conditional_cache_on_arguments(condition=cache_on)
1012 def _tree_items(_repo_id, _tree_id):
1018 def _tree_items(_repo_id, _tree_id):
1013
1019
1014 repo_init = self._factory.repo_libgit2(wire)
1020 repo_init = self._factory.repo_libgit2(wire)
1015 with repo_init as repo:
1021 with repo_init as repo:
1016 try:
1022 try:
1017 tree = repo[tree_id]
1023 tree = repo[tree_id]
1018 except KeyError:
1024 except KeyError:
1019 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1025 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1020
1026
1021 result = []
1027 result = []
1022 for item in tree:
1028 for item in tree:
1023 item_sha = item.hex
1029 item_sha = item.hex
1024 item_mode = item.filemode
1030 item_mode = item.filemode
1025 item_type = item.type_str
1031 item_type = item.type_str
1026
1032
1027 if item_type == 'commit':
1033 if item_type == 'commit':
1028 # NOTE(marcink): submodules we translate to 'link' for backward compat
1034 # NOTE(marcink): submodules we translate to 'link' for backward compat
1029 item_type = 'link'
1035 item_type = 'link'
1030
1036
1031 result.append((item.name, item_mode, item_sha, item_type))
1037 result.append((item.name, item_mode, item_sha, item_type))
1032 return result
1038 return result
1033 return _tree_items(repo_id, tree_id)
1039 return _tree_items(repo_id, tree_id)
1034
1040
1035 @reraise_safe_exceptions
1041 @reraise_safe_exceptions
1036 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1042 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1037 """
1043 """
1038 Old version that uses subprocess to call diff
1044 Old version that uses subprocess to call diff
1039 """
1045 """
1040
1046
1041 flags = [
1047 flags = [
1042 '-U%s' % context, '--patch',
1048 '-U%s' % context, '--patch',
1043 '--binary',
1049 '--binary',
1044 '--find-renames',
1050 '--find-renames',
1045 '--no-indent-heuristic',
1051 '--no-indent-heuristic',
1046 # '--indent-heuristic',
1052 # '--indent-heuristic',
1047 #'--full-index',
1053 #'--full-index',
1048 #'--abbrev=40'
1054 #'--abbrev=40'
1049 ]
1055 ]
1050
1056
1051 if opt_ignorews:
1057 if opt_ignorews:
1052 flags.append('--ignore-all-space')
1058 flags.append('--ignore-all-space')
1053
1059
1054 if commit_id_1 == self.EMPTY_COMMIT:
1060 if commit_id_1 == self.EMPTY_COMMIT:
1055 cmd = ['show'] + flags + [commit_id_2]
1061 cmd = ['show'] + flags + [commit_id_2]
1056 else:
1062 else:
1057 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1063 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1058
1064
1059 if file_filter:
1065 if file_filter:
1060 cmd.extend(['--', file_filter])
1066 cmd.extend(['--', file_filter])
1061
1067
1062 diff, __ = self.run_git_command(wire, cmd)
1068 diff, __ = self.run_git_command(wire, cmd)
1063 # If we used 'show' command, strip first few lines (until actual diff
1069 # If we used 'show' command, strip first few lines (until actual diff
1064 # starts)
1070 # starts)
1065 if commit_id_1 == self.EMPTY_COMMIT:
1071 if commit_id_1 == self.EMPTY_COMMIT:
1066 lines = diff.splitlines()
1072 lines = diff.splitlines()
1067 x = 0
1073 x = 0
1068 for line in lines:
1074 for line in lines:
1069 if line.startswith('diff'):
1075 if line.startswith(b'diff'):
1070 break
1076 break
1071 x += 1
1077 x += 1
1072 # Append new line just like 'diff' command do
1078 # Append new line just like 'diff' command do
1073 diff = '\n'.join(lines[x:]) + '\n'
1079 diff = '\n'.join(lines[x:]) + '\n'
1074 return diff
1080 return diff
1075
1081
1076 @reraise_safe_exceptions
1082 @reraise_safe_exceptions
1077 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1083 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1078 repo_init = self._factory.repo_libgit2(wire)
1084 repo_init = self._factory.repo_libgit2(wire)
1079 with repo_init as repo:
1085 with repo_init as repo:
1080 swap = True
1086 swap = True
1081 flags = 0
1087 flags = 0
1082 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1088 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1083
1089
1084 if opt_ignorews:
1090 if opt_ignorews:
1085 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1091 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1086
1092
1087 if commit_id_1 == self.EMPTY_COMMIT:
1093 if commit_id_1 == self.EMPTY_COMMIT:
1088 comm1 = repo[commit_id_2]
1094 comm1 = repo[commit_id_2]
1089 diff_obj = comm1.tree.diff_to_tree(
1095 diff_obj = comm1.tree.diff_to_tree(
1090 flags=flags, context_lines=context, swap=swap)
1096 flags=flags, context_lines=context, swap=swap)
1091
1097
1092 else:
1098 else:
1093 comm1 = repo[commit_id_2]
1099 comm1 = repo[commit_id_2]
1094 comm2 = repo[commit_id_1]
1100 comm2 = repo[commit_id_1]
1095 diff_obj = comm1.tree.diff_to_tree(
1101 diff_obj = comm1.tree.diff_to_tree(
1096 comm2.tree, flags=flags, context_lines=context, swap=swap)
1102 comm2.tree, flags=flags, context_lines=context, swap=swap)
1097 similar_flags = 0
1103 similar_flags = 0
1098 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1104 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1099 diff_obj.find_similar(flags=similar_flags)
1105 diff_obj.find_similar(flags=similar_flags)
1100
1106
1101 if file_filter:
1107 if file_filter:
1102 for p in diff_obj:
1108 for p in diff_obj:
1103 if p.delta.old_file.path == file_filter:
1109 if p.delta.old_file.path == file_filter:
1104 return p.patch or ''
1110 return p.patch or ''
1105 # fo matching path == no diff
1111 # fo matching path == no diff
1106 return ''
1112 return ''
1107 return diff_obj.patch or ''
1113 return diff_obj.patch or ''
1108
1114
1109 @reraise_safe_exceptions
1115 @reraise_safe_exceptions
1110 def node_history(self, wire, commit_id, path, limit):
1116 def node_history(self, wire, commit_id, path, limit):
1111 cache_on, context_uid, repo_id = self._cache_on(wire)
1117 cache_on, context_uid, repo_id = self._cache_on(wire)
1112 region = self._region(wire)
1118 region = self._region(wire)
1119
1113 @region.conditional_cache_on_arguments(condition=cache_on)
1120 @region.conditional_cache_on_arguments(condition=cache_on)
1114 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1121 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1115 # optimize for n==1, rev-list is much faster for that use-case
1122 # optimize for n==1, rev-list is much faster for that use-case
1116 if limit == 1:
1123 if limit == 1:
1117 cmd = ['rev-list', '-1', commit_id, '--', path]
1124 cmd = ['rev-list', '-1', commit_id, '--', path]
1118 else:
1125 else:
1119 cmd = ['log']
1126 cmd = ['log']
1120 if limit:
1127 if limit:
1121 cmd.extend(['-n', str(safe_int(limit, 0))])
1128 cmd.extend(['-n', str(safe_int(limit, 0))])
1122 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1129 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1123
1130
1124 output, __ = self.run_git_command(wire, cmd)
1131 output, __ = self.run_git_command(wire, cmd)
1125 commit_ids = re.findall(r'[0-9a-fA-F]{40}', output)
1132 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1126
1133
1127 return [x for x in commit_ids]
1134 return [x for x in commit_ids]
1128 return _node_history(context_uid, repo_id, commit_id, path, limit)
1135 return _node_history(context_uid, repo_id, commit_id, path, limit)
1129
1136
1130 @reraise_safe_exceptions
1137 @reraise_safe_exceptions
1131 def node_annotate(self, wire, commit_id, path):
1138 def node_annotate_legacy(self, wire, commit_id, path):
1132
1139 #note: replaced by pygit2 impelementation
1133 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1140 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1134 # -l ==> outputs long shas (and we need all 40 characters)
1141 # -l ==> outputs long shas (and we need all 40 characters)
1135 # --root ==> doesn't put '^' character for boundaries
1142 # --root ==> doesn't put '^' character for boundaries
1136 # -r commit_id ==> blames for the given commit
1143 # -r commit_id ==> blames for the given commit
1137 output, __ = self.run_git_command(wire, cmd)
1144 output, __ = self.run_git_command(wire, cmd)
1138
1145
1139 result = []
1146 result = []
1140 for i, blame_line in enumerate(output.split('\n')[:-1]):
1147 for i, blame_line in enumerate(output.splitlines()[:-1]):
1141 line_no = i + 1
1148 line_no = i + 1
1142 commit_id, line = re.split(r' ', blame_line, 1)
1149 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1143 result.append((line_no, commit_id, line))
1150 result.append((line_no, blame_commit_id, line))
1151
1144 return result
1152 return result
1145
1153
1146 @reraise_safe_exceptions
1154 @reraise_safe_exceptions
1155 def node_annotate(self, wire, commit_id, path):
1156
1157 result_libgit = []
1158 repo_init = self._factory.repo_libgit2(wire)
1159 with repo_init as repo:
1160 commit = repo[commit_id]
1161 blame_obj = repo.blame(path, newest_commit=commit_id)
1162 for i, line in enumerate(commit.tree[path].data.splitlines()):
1163 line_no = i + 1
1164 hunk = blame_obj.for_line(line_no)
1165 blame_commit_id = hunk.final_commit_id.hex
1166
1167 result_libgit.append((line_no, blame_commit_id, line))
1168
1169 return result_libgit
1170
1171 @reraise_safe_exceptions
1147 def update_server_info(self, wire):
1172 def update_server_info(self, wire):
1148 repo = self._factory.repo(wire)
1173 repo = self._factory.repo(wire)
1149 update_server_info(repo)
1174 update_server_info(repo)
1150
1175
1151 @reraise_safe_exceptions
1176 @reraise_safe_exceptions
1152 def get_all_commit_ids(self, wire):
1177 def get_all_commit_ids(self, wire):
1153
1178
1154 cache_on, context_uid, repo_id = self._cache_on(wire)
1179 cache_on, context_uid, repo_id = self._cache_on(wire)
1155 region = self._region(wire)
1180 region = self._region(wire)
1181
1156 @region.conditional_cache_on_arguments(condition=cache_on)
1182 @region.conditional_cache_on_arguments(condition=cache_on)
1157 def _get_all_commit_ids(_context_uid, _repo_id):
1183 def _get_all_commit_ids(_context_uid, _repo_id):
1158
1184
1159 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1185 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1160 try:
1186 try:
1161 output, __ = self.run_git_command(wire, cmd)
1187 output, __ = self.run_git_command(wire, cmd)
1162 return output.splitlines()
1188 return output.splitlines()
1163 except Exception:
1189 except Exception:
1164 # Can be raised for empty repositories
1190 # Can be raised for empty repositories
1165 return []
1191 return []
1192
1193 @region.conditional_cache_on_arguments(condition=cache_on)
1194 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1195 repo_init = self._factory.repo_libgit2(wire)
1196 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1197 results = []
1198 with repo_init as repo:
1199 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1200 results.append(commit.id.hex)
1201
1166 return _get_all_commit_ids(context_uid, repo_id)
1202 return _get_all_commit_ids(context_uid, repo_id)
1167
1203
1168 @reraise_safe_exceptions
1204 @reraise_safe_exceptions
1169 def run_git_command(self, wire, cmd, **opts):
1205 def run_git_command(self, wire, cmd, **opts):
1170 path = wire.get('path', None)
1206 path = wire.get('path', None)
1171
1207
1172 if path and os.path.isdir(path):
1208 if path and os.path.isdir(path):
1173 opts['cwd'] = path
1209 opts['cwd'] = path
1174
1210
1175 if '_bare' in opts:
1211 if '_bare' in opts:
1176 _copts = []
1212 _copts = []
1177 del opts['_bare']
1213 del opts['_bare']
1178 else:
1214 else:
1179 _copts = ['-c', 'core.quotepath=false', ]
1215 _copts = ['-c', 'core.quotepath=false', ]
1180 safe_call = False
1216 safe_call = False
1181 if '_safe' in opts:
1217 if '_safe' in opts:
1182 # no exc on failure
1218 # no exc on failure
1183 del opts['_safe']
1219 del opts['_safe']
1184 safe_call = True
1220 safe_call = True
1185
1221
1186 if '_copts' in opts:
1222 if '_copts' in opts:
1187 _copts.extend(opts['_copts'] or [])
1223 _copts.extend(opts['_copts'] or [])
1188 del opts['_copts']
1224 del opts['_copts']
1189
1225
1190 gitenv = os.environ.copy()
1226 gitenv = os.environ.copy()
1191 gitenv.update(opts.pop('extra_env', {}))
1227 gitenv.update(opts.pop('extra_env', {}))
1192 # need to clean fix GIT_DIR !
1228 # need to clean fix GIT_DIR !
1193 if 'GIT_DIR' in gitenv:
1229 if 'GIT_DIR' in gitenv:
1194 del gitenv['GIT_DIR']
1230 del gitenv['GIT_DIR']
1195 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1231 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1196 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1232 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1197
1233
1198 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1234 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1199 _opts = {'env': gitenv, 'shell': False}
1235 _opts = {'env': gitenv, 'shell': False}
1200
1236
1201 proc = None
1237 proc = None
1202 try:
1238 try:
1203 _opts.update(opts)
1239 _opts.update(opts)
1204 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1240 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1205
1241
1206 return ''.join(proc), ''.join(proc.error)
1242 return b''.join(proc), b''.join(proc.stderr)
1207 except (EnvironmentError, OSError) as err:
1243 except OSError as err:
1208 cmd = ' '.join(cmd) # human friendly CMD
1244 cmd = ' '.join(cmd) # human friendly CMD
1209 tb_err = ("Couldn't run git command (%s).\n"
1245 tb_err = ("Couldn't run git command (%s).\n"
1210 "Original error was:%s\n"
1246 "Original error was:%s\n"
1211 "Call options:%s\n"
1247 "Call options:%s\n"
1212 % (cmd, err, _opts))
1248 % (cmd, err, _opts))
1213 log.exception(tb_err)
1249 log.exception(tb_err)
1214 if safe_call:
1250 if safe_call:
1215 return '', err
1251 return '', err
1216 else:
1252 else:
1217 raise exceptions.VcsException()(tb_err)
1253 raise exceptions.VcsException()(tb_err)
1218 finally:
1254 finally:
1219 if proc:
1255 if proc:
1220 proc.close()
1256 proc.close()
1221
1257
1222 @reraise_safe_exceptions
1258 @reraise_safe_exceptions
1223 def install_hooks(self, wire, force=False):
1259 def install_hooks(self, wire, force=False):
1224 from vcsserver.hook_utils import install_git_hooks
1260 from vcsserver.hook_utils import install_git_hooks
1225 bare = self.bare(wire)
1261 bare = self.bare(wire)
1226 path = wire['path']
1262 path = wire['path']
1227 return install_git_hooks(path, bare, force_create=force)
1263 return install_git_hooks(path, bare, force_create=force)
1228
1264
1229 @reraise_safe_exceptions
1265 @reraise_safe_exceptions
1230 def get_hooks_info(self, wire):
1266 def get_hooks_info(self, wire):
1231 from vcsserver.hook_utils import (
1267 from vcsserver.hook_utils import (
1232 get_git_pre_hook_version, get_git_post_hook_version)
1268 get_git_pre_hook_version, get_git_post_hook_version)
1233 bare = self.bare(wire)
1269 bare = self.bare(wire)
1234 path = wire['path']
1270 path = wire['path']
1235 return {
1271 return {
1236 'pre_version': get_git_pre_hook_version(path, bare),
1272 'pre_version': get_git_pre_hook_version(path, bare),
1237 'post_version': get_git_post_hook_version(path, bare),
1273 'post_version': get_git_post_hook_version(path, bare),
1238 }
1274 }
1239
1275
1240 @reraise_safe_exceptions
1276 @reraise_safe_exceptions
1241 def set_head_ref(self, wire, head_name):
1277 def set_head_ref(self, wire, head_name):
1242 log.debug('Setting refs/head to `%s`', head_name)
1278 log.debug('Setting refs/head to `%s`', head_name)
1243 cmd = ['symbolic-ref', '"HEAD"', '"refs/heads/%s"' % head_name]
1279 cmd = ['symbolic-ref', '"HEAD"', '"refs/heads/%s"' % head_name]
1244 output, __ = self.run_git_command(wire, cmd)
1280 output, __ = self.run_git_command(wire, cmd)
1245 return [head_name] + output.splitlines()
1281 return [head_name] + output.splitlines()
1246
1282
1247 @reraise_safe_exceptions
1283 @reraise_safe_exceptions
1248 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1284 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1249 archive_dir_name, commit_id):
1285 archive_dir_name, commit_id):
1250
1286
1251 def file_walker(_commit_id, path):
1287 def file_walker(_commit_id, path):
1252 repo_init = self._factory.repo_libgit2(wire)
1288 repo_init = self._factory.repo_libgit2(wire)
1253
1289
1254 with repo_init as repo:
1290 with repo_init as repo:
1255 commit = repo[commit_id]
1291 commit = repo[commit_id]
1256
1292
1257 if path in ['', '/']:
1293 if path in ['', '/']:
1258 tree = commit.tree
1294 tree = commit.tree
1259 else:
1295 else:
1260 tree = commit.tree[path.rstrip('/')]
1296 tree = commit.tree[path.rstrip('/')]
1261 tree_id = tree.id.hex
1297 tree_id = tree.id.hex
1262 try:
1298 try:
1263 tree = repo[tree_id]
1299 tree = repo[tree_id]
1264 except KeyError:
1300 except KeyError:
1265 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1301 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1266
1302
1267 index = LibGit2Index.Index()
1303 index = LibGit2Index.Index()
1268 index.read_tree(tree)
1304 index.read_tree(tree)
1269 file_iter = index
1305 file_iter = index
1270
1306
1271 for fn in file_iter:
1307 for fn in file_iter:
1272 file_path = fn.path
1308 file_path = fn.path
1273 mode = fn.mode
1309 mode = fn.mode
1274 is_link = stat.S_ISLNK(mode)
1310 is_link = stat.S_ISLNK(mode)
1275 if mode == pygit2.GIT_FILEMODE_COMMIT:
1311 if mode == pygit2.GIT_FILEMODE_COMMIT:
1276 log.debug('Skipping path %s as a commit node', file_path)
1312 log.debug('Skipping path %s as a commit node', file_path)
1277 continue
1313 continue
1278 yield ArchiveNode(file_path, mode, is_link, repo[fn.hex].read_raw)
1314 yield ArchiveNode(file_path, mode, is_link, repo[fn.hex].read_raw)
1279
1315
1280 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1316 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1281 archive_dir_name, commit_id)
1317 archive_dir_name, commit_id)
@@ -1,1046 +1,1057 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import io
18 import io
19 import logging
19 import logging
20 import stat
20 import stat
21 import urllib.request, urllib.parse, urllib.error
21 import urllib.request, urllib.parse, urllib.error
22 import urllib.request, urllib.error, urllib.parse
22 import urllib.request, urllib.error, urllib.parse
23 import traceback
23 import traceback
24
24
25 from hgext import largefiles, rebase, purge
25 from hgext import largefiles, rebase, purge
26
26
27 from mercurial import commands
27 from mercurial import commands
28 from mercurial import unionrepo
28 from mercurial import unionrepo
29 from mercurial import verify
29 from mercurial import verify
30 from mercurial import repair
30 from mercurial import repair
31
31
32 import vcsserver
32 import vcsserver
33 from vcsserver import exceptions
33 from vcsserver import exceptions
34 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original, archive_repo, ArchiveNode
34 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original, archive_repo, ArchiveNode
35 from vcsserver.hgcompat import (
35 from vcsserver.hgcompat import (
36 archival, bin, clone, config as hgconfig, diffopts, hex, get_ctx,
36 archival, bin, clone, config as hgconfig, diffopts, hex, get_ctx,
37 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
37 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
38 makepeer, instance, match, memctx, exchange, memfilectx, nullrev, hg_merge,
38 makepeer, instance, match, memctx, exchange, memfilectx, nullrev, hg_merge,
39 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
39 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
40 RepoLookupError, InterventionRequired, RequirementError,
40 RepoLookupError, InterventionRequired, RequirementError,
41 alwaysmatcher, patternmatcher, hgutil, hgext_strip)
41 alwaysmatcher, patternmatcher, hgutil, hgext_strip)
42 from vcsserver.utils import ascii_bytes, ascii_str, safe_str
42 from vcsserver.vcs_base import RemoteBase
43 from vcsserver.vcs_base import RemoteBase
43
44
44 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
45
46
46
47
47 def make_ui_from_config(repo_config):
48 def make_ui_from_config(repo_config):
48
49
49 class LoggingUI(ui.ui):
50 class LoggingUI(ui.ui):
51
50 def status(self, *msg, **opts):
52 def status(self, *msg, **opts):
51 log.info(' '.join(msg).rstrip('\n'))
53 str_msg = map(safe_str, msg)
52 super(LoggingUI, self).status(*msg, **opts)
54 log.info(' '.join(str_msg).rstrip('\n'))
55 #super(LoggingUI, self).status(*msg, **opts)
53
56
54 def warn(self, *msg, **opts):
57 def warn(self, *msg, **opts):
55 log.warn(' '.join(msg).rstrip('\n'))
58 str_msg = map(safe_str, msg)
56 super(LoggingUI, self).warn(*msg, **opts)
59 log.warning('ui_logger:'+' '.join(str_msg).rstrip('\n'))
60 #super(LoggingUI, self).warn(*msg, **opts)
57
61
58 def error(self, *msg, **opts):
62 def error(self, *msg, **opts):
59 log.error(' '.join(msg).rstrip('\n'))
63 str_msg = map(safe_str, msg)
60 super(LoggingUI, self).error(*msg, **opts)
64 log.error('ui_logger:'+' '.join(str_msg).rstrip('\n'))
65 #super(LoggingUI, self).error(*msg, **opts)
61
66
62 def note(self, *msg, **opts):
67 def note(self, *msg, **opts):
63 log.info(' '.join(msg).rstrip('\n'))
68 str_msg = map(safe_str, msg)
64 super(LoggingUI, self).note(*msg, **opts)
69 log.info('ui_logger:'+' '.join(str_msg).rstrip('\n'))
70 #super(LoggingUI, self).note(*msg, **opts)
65
71
66 def debug(self, *msg, **opts):
72 def debug(self, *msg, **opts):
67 log.debug(' '.join(msg).rstrip('\n'))
73 str_msg = map(safe_str, msg)
68 super(LoggingUI, self).debug(*msg, **opts)
74 log.debug('ui_logger:'+' '.join(str_msg).rstrip('\n'))
75 #super(LoggingUI, self).debug(*msg, **opts)
69
76
70 baseui = LoggingUI()
77 baseui = LoggingUI()
71
78
72 # clean the baseui object
79 # clean the baseui object
73 baseui._ocfg = hgconfig.config()
80 baseui._ocfg = hgconfig.config()
74 baseui._ucfg = hgconfig.config()
81 baseui._ucfg = hgconfig.config()
75 baseui._tcfg = hgconfig.config()
82 baseui._tcfg = hgconfig.config()
76
83
77 for section, option, value in repo_config:
84 for section, option, value in repo_config:
78 baseui.setconfig(section, option, value)
85 baseui.setconfig(ascii_bytes(section), ascii_bytes(option), ascii_bytes(value))
79
86
80 # make our hgweb quiet so it doesn't print output
87 # make our hgweb quiet so it doesn't print output
81 baseui.setconfig('ui', 'quiet', 'true')
88 baseui.setconfig(b'ui', b'quiet', b'true')
82
89
83 baseui.setconfig('ui', 'paginate', 'never')
90 baseui.setconfig(b'ui', b'paginate', b'never')
84 # for better Error reporting of Mercurial
91 # for better Error reporting of Mercurial
85 baseui.setconfig('ui', 'message-output', 'stderr')
92 baseui.setconfig(b'ui', b'message-output', b'stderr')
86
93
87 # force mercurial to only use 1 thread, otherwise it may try to set a
94 # force mercurial to only use 1 thread, otherwise it may try to set a
88 # signal in a non-main thread, thus generating a ValueError.
95 # signal in a non-main thread, thus generating a ValueError.
89 baseui.setconfig('worker', 'numcpus', 1)
96 baseui.setconfig(b'worker', b'numcpus', 1)
90
97
91 # If there is no config for the largefiles extension, we explicitly disable
98 # If there is no config for the largefiles extension, we explicitly disable
92 # it here. This overrides settings from repositories hgrc file. Recent
99 # it here. This overrides settings from repositories hgrc file. Recent
93 # mercurial versions enable largefiles in hgrc on clone from largefile
100 # mercurial versions enable largefiles in hgrc on clone from largefile
94 # repo.
101 # repo.
95 if not baseui.hasconfig('extensions', 'largefiles'):
102 if not baseui.hasconfig(b'extensions', b'largefiles'):
96 log.debug('Explicitly disable largefiles extension for repo.')
103 log.debug('Explicitly disable largefiles extension for repo.')
97 baseui.setconfig('extensions', 'largefiles', '!')
104 baseui.setconfig(b'extensions', b'largefiles', b'!')
98
105
99 return baseui
106 return baseui
100
107
101
108
102 def reraise_safe_exceptions(func):
109 def reraise_safe_exceptions(func):
103 """Decorator for converting mercurial exceptions to something neutral."""
110 """Decorator for converting mercurial exceptions to something neutral."""
104
111
105 def wrapper(*args, **kwargs):
112 def wrapper(*args, **kwargs):
106 try:
113 try:
107 return func(*args, **kwargs)
114 return func(*args, **kwargs)
108 except (Abort, InterventionRequired) as e:
115 except (Abort, InterventionRequired) as e:
109 raise_from_original(exceptions.AbortException(e))
116 raise_from_original(exceptions.AbortException(e), e)
110 except RepoLookupError as e:
117 except RepoLookupError as e:
111 raise_from_original(exceptions.LookupException(e))
118 raise_from_original(exceptions.LookupException(e), e)
112 except RequirementError as e:
119 except RequirementError as e:
113 raise_from_original(exceptions.RequirementException(e))
120 raise_from_original(exceptions.RequirementException(e), e)
114 except RepoError as e:
121 except RepoError as e:
115 raise_from_original(exceptions.VcsException(e))
122 raise_from_original(exceptions.VcsException(e), e)
116 except LookupError as e:
123 except LookupError as e:
117 raise_from_original(exceptions.LookupException(e))
124 raise_from_original(exceptions.LookupException(e), e)
118 except Exception as e:
125 except Exception as e:
119 if not hasattr(e, '_vcs_kind'):
126 if not hasattr(e, '_vcs_kind'):
120 log.exception("Unhandled exception in hg remote call")
127 log.exception("Unhandled exception in hg remote call")
121 raise_from_original(exceptions.UnhandledException(e))
128 raise_from_original(exceptions.UnhandledException(e), e)
122
129
123 raise
130 raise
124 return wrapper
131 return wrapper
125
132
126
133
127 class MercurialFactory(RepoFactory):
134 class MercurialFactory(RepoFactory):
128 repo_type = 'hg'
135 repo_type = 'hg'
129
136
130 def _create_config(self, config, hooks=True):
137 def _create_config(self, config, hooks=True):
131 if not hooks:
138 if not hooks:
132 hooks_to_clean = frozenset((
139 hooks_to_clean = frozenset((
133 'changegroup.repo_size', 'preoutgoing.pre_pull',
140 'changegroup.repo_size', 'preoutgoing.pre_pull',
134 'outgoing.pull_logger', 'prechangegroup.pre_push'))
141 'outgoing.pull_logger', 'prechangegroup.pre_push'))
135 new_config = []
142 new_config = []
136 for section, option, value in config:
143 for section, option, value in config:
137 if section == 'hooks' and option in hooks_to_clean:
144 if section == 'hooks' and option in hooks_to_clean:
138 continue
145 continue
139 new_config.append((section, option, value))
146 new_config.append((section, option, value))
140 config = new_config
147 config = new_config
141
148
142 baseui = make_ui_from_config(config)
149 baseui = make_ui_from_config(config)
143 return baseui
150 return baseui
144
151
145 def _create_repo(self, wire, create):
152 def _create_repo(self, wire, create):
146 baseui = self._create_config(wire["config"])
153 baseui = self._create_config(wire["config"])
147 return instance(baseui, wire["path"], create)
154 return instance(baseui, ascii_bytes(wire["path"]), create)
148
155
149 def repo(self, wire, create=False):
156 def repo(self, wire, create=False):
150 """
157 """
151 Get a repository instance for the given path.
158 Get a repository instance for the given path.
152 """
159 """
153 return self._create_repo(wire, create)
160 return self._create_repo(wire, create)
154
161
155
162
156 def patch_ui_message_output(baseui):
163 def patch_ui_message_output(baseui):
157 baseui.setconfig('ui', 'quiet', 'false')
164 baseui.setconfig(b'ui', b'quiet', b'false')
158 output = io.BytesIO()
165 output = io.BytesIO()
159
166
160 def write(data, **unused_kwargs):
167 def write(data, **unused_kwargs):
161 output.write(data)
168 output.write(data)
162
169
163 baseui.status = write
170 baseui.status = write
164 baseui.write = write
171 baseui.write = write
165 baseui.warn = write
172 baseui.warn = write
166 baseui.debug = write
173 baseui.debug = write
167
174
168 return baseui, output
175 return baseui, output
169
176
170
177
171 class HgRemote(RemoteBase):
178 class HgRemote(RemoteBase):
172
179
173 def __init__(self, factory):
180 def __init__(self, factory):
174 self._factory = factory
181 self._factory = factory
175 self._bulk_methods = {
182 self._bulk_methods = {
176 "affected_files": self.ctx_files,
183 "affected_files": self.ctx_files,
177 "author": self.ctx_user,
184 "author": self.ctx_user,
178 "branch": self.ctx_branch,
185 "branch": self.ctx_branch,
179 "children": self.ctx_children,
186 "children": self.ctx_children,
180 "date": self.ctx_date,
187 "date": self.ctx_date,
181 "message": self.ctx_description,
188 "message": self.ctx_description,
182 "parents": self.ctx_parents,
189 "parents": self.ctx_parents,
183 "status": self.ctx_status,
190 "status": self.ctx_status,
184 "obsolete": self.ctx_obsolete,
191 "obsolete": self.ctx_obsolete,
185 "phase": self.ctx_phase,
192 "phase": self.ctx_phase,
186 "hidden": self.ctx_hidden,
193 "hidden": self.ctx_hidden,
187 "_file_paths": self.ctx_list,
194 "_file_paths": self.ctx_list,
188 }
195 }
189
196
190 def _get_ctx(self, repo, ref):
197 def _get_ctx(self, repo, ref):
191 return get_ctx(repo, ref)
198 return get_ctx(repo, ref)
192
199
193 @reraise_safe_exceptions
200 @reraise_safe_exceptions
194 def discover_hg_version(self):
201 def discover_hg_version(self):
195 from mercurial import util
202 from mercurial import util
196 return util.version()
203 return util.version()
197
204
198 @reraise_safe_exceptions
205 @reraise_safe_exceptions
199 def is_empty(self, wire):
206 def is_empty(self, wire):
200 repo = self._factory.repo(wire)
207 repo = self._factory.repo(wire)
201
208
202 try:
209 try:
203 return len(repo) == 0
210 return len(repo) == 0
204 except Exception:
211 except Exception:
205 log.exception("failed to read object_store")
212 log.exception("failed to read object_store")
206 return False
213 return False
207
214
208 @reraise_safe_exceptions
215 @reraise_safe_exceptions
209 def bookmarks(self, wire):
216 def bookmarks(self, wire):
210 cache_on, context_uid, repo_id = self._cache_on(wire)
217 cache_on, context_uid, repo_id = self._cache_on(wire)
211 region = self._region(wire)
218 region = self._region(wire)
212 @region.conditional_cache_on_arguments(condition=cache_on)
219 @region.conditional_cache_on_arguments(condition=cache_on)
213 def _bookmarks(_context_uid, _repo_id):
220 def _bookmarks(_context_uid, _repo_id):
214 repo = self._factory.repo(wire)
221 repo = self._factory.repo(wire)
215 return dict(repo._bookmarks)
222 return dict(repo._bookmarks)
216
223
217 return _bookmarks(context_uid, repo_id)
224 return _bookmarks(context_uid, repo_id)
218
225
219 @reraise_safe_exceptions
226 @reraise_safe_exceptions
220 def branches(self, wire, normal, closed):
227 def branches(self, wire, normal, closed):
221 cache_on, context_uid, repo_id = self._cache_on(wire)
228 cache_on, context_uid, repo_id = self._cache_on(wire)
222 region = self._region(wire)
229 region = self._region(wire)
223 @region.conditional_cache_on_arguments(condition=cache_on)
230 @region.conditional_cache_on_arguments(condition=cache_on)
224 def _branches(_context_uid, _repo_id, _normal, _closed):
231 def _branches(_context_uid, _repo_id, _normal, _closed):
225 repo = self._factory.repo(wire)
232 repo = self._factory.repo(wire)
226 iter_branches = repo.branchmap().iterbranches()
233 iter_branches = repo.branchmap().iterbranches()
227 bt = {}
234 bt = {}
228 for branch_name, _heads, tip, is_closed in iter_branches:
235 for branch_name, _heads, tip, is_closed in iter_branches:
229 if normal and not is_closed:
236 if normal and not is_closed:
230 bt[branch_name] = tip
237 bt[branch_name] = tip
231 if closed and is_closed:
238 if closed and is_closed:
232 bt[branch_name] = tip
239 bt[branch_name] = tip
233
240
234 return bt
241 return bt
235
242
236 return _branches(context_uid, repo_id, normal, closed)
243 return _branches(context_uid, repo_id, normal, closed)
237
244
238 @reraise_safe_exceptions
245 @reraise_safe_exceptions
239 def bulk_request(self, wire, commit_id, pre_load):
246 def bulk_request(self, wire, commit_id, pre_load):
240 cache_on, context_uid, repo_id = self._cache_on(wire)
247 cache_on, context_uid, repo_id = self._cache_on(wire)
241 region = self._region(wire)
248 region = self._region(wire)
242 @region.conditional_cache_on_arguments(condition=cache_on)
249 @region.conditional_cache_on_arguments(condition=cache_on)
243 def _bulk_request(_repo_id, _commit_id, _pre_load):
250 def _bulk_request(_repo_id, _commit_id, _pre_load):
244 result = {}
251 result = {}
245 for attr in pre_load:
252 for attr in pre_load:
246 try:
253 try:
247 method = self._bulk_methods[attr]
254 method = self._bulk_methods[attr]
248 result[attr] = method(wire, commit_id)
255 result[attr] = method(wire, commit_id)
249 except KeyError as e:
256 except KeyError as e:
250 raise exceptions.VcsException(e)(
257 raise exceptions.VcsException(e)(
251 'Unknown bulk attribute: "%s"' % attr)
258 'Unknown bulk attribute: "%s"' % attr)
252 return result
259 return result
253
260
254 return _bulk_request(repo_id, commit_id, sorted(pre_load))
261 return _bulk_request(repo_id, commit_id, sorted(pre_load))
255
262
256 @reraise_safe_exceptions
263 @reraise_safe_exceptions
257 def ctx_branch(self, wire, commit_id):
264 def ctx_branch(self, wire, commit_id):
258 cache_on, context_uid, repo_id = self._cache_on(wire)
265 cache_on, context_uid, repo_id = self._cache_on(wire)
259 region = self._region(wire)
266 region = self._region(wire)
260 @region.conditional_cache_on_arguments(condition=cache_on)
267 @region.conditional_cache_on_arguments(condition=cache_on)
261 def _ctx_branch(_repo_id, _commit_id):
268 def _ctx_branch(_repo_id, _commit_id):
262 repo = self._factory.repo(wire)
269 repo = self._factory.repo(wire)
263 ctx = self._get_ctx(repo, commit_id)
270 ctx = self._get_ctx(repo, commit_id)
264 return ctx.branch()
271 return ctx.branch()
265 return _ctx_branch(repo_id, commit_id)
272 return _ctx_branch(repo_id, commit_id)
266
273
267 @reraise_safe_exceptions
274 @reraise_safe_exceptions
268 def ctx_date(self, wire, commit_id):
275 def ctx_date(self, wire, commit_id):
269 cache_on, context_uid, repo_id = self._cache_on(wire)
276 cache_on, context_uid, repo_id = self._cache_on(wire)
270 region = self._region(wire)
277 region = self._region(wire)
271 @region.conditional_cache_on_arguments(condition=cache_on)
278 @region.conditional_cache_on_arguments(condition=cache_on)
272 def _ctx_date(_repo_id, _commit_id):
279 def _ctx_date(_repo_id, _commit_id):
273 repo = self._factory.repo(wire)
280 repo = self._factory.repo(wire)
274 ctx = self._get_ctx(repo, commit_id)
281 ctx = self._get_ctx(repo, commit_id)
275 return ctx.date()
282 return ctx.date()
276 return _ctx_date(repo_id, commit_id)
283 return _ctx_date(repo_id, commit_id)
277
284
278 @reraise_safe_exceptions
285 @reraise_safe_exceptions
279 def ctx_description(self, wire, revision):
286 def ctx_description(self, wire, revision):
280 repo = self._factory.repo(wire)
287 repo = self._factory.repo(wire)
281 ctx = self._get_ctx(repo, revision)
288 ctx = self._get_ctx(repo, revision)
282 return ctx.description()
289 return ctx.description()
283
290
284 @reraise_safe_exceptions
291 @reraise_safe_exceptions
285 def ctx_files(self, wire, commit_id):
292 def ctx_files(self, wire, commit_id):
286 cache_on, context_uid, repo_id = self._cache_on(wire)
293 cache_on, context_uid, repo_id = self._cache_on(wire)
287 region = self._region(wire)
294 region = self._region(wire)
288 @region.conditional_cache_on_arguments(condition=cache_on)
295 @region.conditional_cache_on_arguments(condition=cache_on)
289 def _ctx_files(_repo_id, _commit_id):
296 def _ctx_files(_repo_id, _commit_id):
290 repo = self._factory.repo(wire)
297 repo = self._factory.repo(wire)
291 ctx = self._get_ctx(repo, commit_id)
298 ctx = self._get_ctx(repo, commit_id)
292 return ctx.files()
299 return ctx.files()
293
300
294 return _ctx_files(repo_id, commit_id)
301 return _ctx_files(repo_id, commit_id)
295
302
296 @reraise_safe_exceptions
303 @reraise_safe_exceptions
297 def ctx_list(self, path, revision):
304 def ctx_list(self, path, revision):
298 repo = self._factory.repo(path)
305 repo = self._factory.repo(path)
299 ctx = self._get_ctx(repo, revision)
306 ctx = self._get_ctx(repo, revision)
300 return list(ctx)
307 return list(ctx)
301
308
302 @reraise_safe_exceptions
309 @reraise_safe_exceptions
303 def ctx_parents(self, wire, commit_id):
310 def ctx_parents(self, wire, commit_id):
304 cache_on, context_uid, repo_id = self._cache_on(wire)
311 cache_on, context_uid, repo_id = self._cache_on(wire)
305 region = self._region(wire)
312 region = self._region(wire)
306 @region.conditional_cache_on_arguments(condition=cache_on)
313 @region.conditional_cache_on_arguments(condition=cache_on)
307 def _ctx_parents(_repo_id, _commit_id):
314 def _ctx_parents(_repo_id, _commit_id):
308 repo = self._factory.repo(wire)
315 repo = self._factory.repo(wire)
309 ctx = self._get_ctx(repo, commit_id)
316 ctx = self._get_ctx(repo, commit_id)
310 return [parent.hex() for parent in ctx.parents()
317 return [parent.hex() for parent in ctx.parents()
311 if not (parent.hidden() or parent.obsolete())]
318 if not (parent.hidden() or parent.obsolete())]
312
319
313 return _ctx_parents(repo_id, commit_id)
320 return _ctx_parents(repo_id, commit_id)
314
321
315 @reraise_safe_exceptions
322 @reraise_safe_exceptions
316 def ctx_children(self, wire, commit_id):
323 def ctx_children(self, wire, commit_id):
317 cache_on, context_uid, repo_id = self._cache_on(wire)
324 cache_on, context_uid, repo_id = self._cache_on(wire)
318 region = self._region(wire)
325 region = self._region(wire)
319 @region.conditional_cache_on_arguments(condition=cache_on)
326 @region.conditional_cache_on_arguments(condition=cache_on)
320 def _ctx_children(_repo_id, _commit_id):
327 def _ctx_children(_repo_id, _commit_id):
321 repo = self._factory.repo(wire)
328 repo = self._factory.repo(wire)
322 ctx = self._get_ctx(repo, commit_id)
329 ctx = self._get_ctx(repo, commit_id)
323 return [child.hex() for child in ctx.children()
330 return [child.hex() for child in ctx.children()
324 if not (child.hidden() or child.obsolete())]
331 if not (child.hidden() or child.obsolete())]
325
332
326 return _ctx_children(repo_id, commit_id)
333 return _ctx_children(repo_id, commit_id)
327
334
328 @reraise_safe_exceptions
335 @reraise_safe_exceptions
329 def ctx_phase(self, wire, commit_id):
336 def ctx_phase(self, wire, commit_id):
330 cache_on, context_uid, repo_id = self._cache_on(wire)
337 cache_on, context_uid, repo_id = self._cache_on(wire)
331 region = self._region(wire)
338 region = self._region(wire)
332 @region.conditional_cache_on_arguments(condition=cache_on)
339 @region.conditional_cache_on_arguments(condition=cache_on)
333 def _ctx_phase(_context_uid, _repo_id, _commit_id):
340 def _ctx_phase(_context_uid, _repo_id, _commit_id):
334 repo = self._factory.repo(wire)
341 repo = self._factory.repo(wire)
335 ctx = self._get_ctx(repo, commit_id)
342 ctx = self._get_ctx(repo, commit_id)
336 # public=0, draft=1, secret=3
343 # public=0, draft=1, secret=3
337 return ctx.phase()
344 return ctx.phase()
338 return _ctx_phase(context_uid, repo_id, commit_id)
345 return _ctx_phase(context_uid, repo_id, commit_id)
339
346
340 @reraise_safe_exceptions
347 @reraise_safe_exceptions
341 def ctx_obsolete(self, wire, commit_id):
348 def ctx_obsolete(self, wire, commit_id):
342 cache_on, context_uid, repo_id = self._cache_on(wire)
349 cache_on, context_uid, repo_id = self._cache_on(wire)
343 region = self._region(wire)
350 region = self._region(wire)
344 @region.conditional_cache_on_arguments(condition=cache_on)
351 @region.conditional_cache_on_arguments(condition=cache_on)
345 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
352 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
346 repo = self._factory.repo(wire)
353 repo = self._factory.repo(wire)
347 ctx = self._get_ctx(repo, commit_id)
354 ctx = self._get_ctx(repo, commit_id)
348 return ctx.obsolete()
355 return ctx.obsolete()
349 return _ctx_obsolete(context_uid, repo_id, commit_id)
356 return _ctx_obsolete(context_uid, repo_id, commit_id)
350
357
351 @reraise_safe_exceptions
358 @reraise_safe_exceptions
352 def ctx_hidden(self, wire, commit_id):
359 def ctx_hidden(self, wire, commit_id):
353 cache_on, context_uid, repo_id = self._cache_on(wire)
360 cache_on, context_uid, repo_id = self._cache_on(wire)
354 region = self._region(wire)
361 region = self._region(wire)
355 @region.conditional_cache_on_arguments(condition=cache_on)
362 @region.conditional_cache_on_arguments(condition=cache_on)
356 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
363 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
357 repo = self._factory.repo(wire)
364 repo = self._factory.repo(wire)
358 ctx = self._get_ctx(repo, commit_id)
365 ctx = self._get_ctx(repo, commit_id)
359 return ctx.hidden()
366 return ctx.hidden()
360 return _ctx_hidden(context_uid, repo_id, commit_id)
367 return _ctx_hidden(context_uid, repo_id, commit_id)
361
368
362 @reraise_safe_exceptions
369 @reraise_safe_exceptions
363 def ctx_substate(self, wire, revision):
370 def ctx_substate(self, wire, revision):
364 repo = self._factory.repo(wire)
371 repo = self._factory.repo(wire)
365 ctx = self._get_ctx(repo, revision)
372 ctx = self._get_ctx(repo, revision)
366 return ctx.substate
373 return ctx.substate
367
374
368 @reraise_safe_exceptions
375 @reraise_safe_exceptions
369 def ctx_status(self, wire, revision):
376 def ctx_status(self, wire, revision):
370 repo = self._factory.repo(wire)
377 repo = self._factory.repo(wire)
371 ctx = self._get_ctx(repo, revision)
378 ctx = self._get_ctx(repo, revision)
372 status = repo[ctx.p1().node()].status(other=ctx.node())
379 status = repo[ctx.p1().node()].status(other=ctx.node())
373 # object of status (odd, custom named tuple in mercurial) is not
380 # object of status (odd, custom named tuple in mercurial) is not
374 # correctly serializable, we make it a list, as the underling
381 # correctly serializable, we make it a list, as the underling
375 # API expects this to be a list
382 # API expects this to be a list
376 return list(status)
383 return list(status)
377
384
378 @reraise_safe_exceptions
385 @reraise_safe_exceptions
379 def ctx_user(self, wire, revision):
386 def ctx_user(self, wire, revision):
380 repo = self._factory.repo(wire)
387 repo = self._factory.repo(wire)
381 ctx = self._get_ctx(repo, revision)
388 ctx = self._get_ctx(repo, revision)
382 return ctx.user()
389 return ctx.user()
383
390
384 @reraise_safe_exceptions
391 @reraise_safe_exceptions
385 def check_url(self, url, config):
392 def check_url(self, url, config):
386 _proto = None
393 _proto = None
387 if '+' in url[:url.find('://')]:
394 if '+' in url[:url.find('://')]:
388 _proto = url[0:url.find('+')]
395 _proto = url[0:url.find('+')]
389 url = url[url.find('+') + 1:]
396 url = url[url.find('+') + 1:]
390 handlers = []
397 handlers = []
391 url_obj = url_parser(url)
398 url_obj = url_parser(url)
392 test_uri, authinfo = url_obj.authinfo()
399 test_uri, authinfo = url_obj.authinfo()
393 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
400 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
394 url_obj.query = obfuscate_qs(url_obj.query)
401 url_obj.query = obfuscate_qs(url_obj.query)
395
402
396 cleaned_uri = str(url_obj)
403 cleaned_uri = str(url_obj)
397 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
404 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
398
405
399 if authinfo:
406 if authinfo:
400 # create a password manager
407 # create a password manager
401 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
408 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
402 passmgr.add_password(*authinfo)
409 passmgr.add_password(*authinfo)
403
410
404 handlers.extend((httpbasicauthhandler(passmgr),
411 handlers.extend((httpbasicauthhandler(passmgr),
405 httpdigestauthhandler(passmgr)))
412 httpdigestauthhandler(passmgr)))
406
413
407 o = urllib.request.build_opener(*handlers)
414 o = urllib.request.build_opener(*handlers)
408 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
415 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
409 ('Accept', 'application/mercurial-0.1')]
416 ('Accept', 'application/mercurial-0.1')]
410
417
411 q = {"cmd": 'between'}
418 q = {"cmd": 'between'}
412 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
419 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
413 qs = '?%s' % urllib.parse.urlencode(q)
420 qs = '?%s' % urllib.parse.urlencode(q)
414 cu = "%s%s" % (test_uri, qs)
421 cu = "%s%s" % (test_uri, qs)
415 req = urllib.request.Request(cu, None, {})
422 req = urllib.request.Request(cu, None, {})
416
423
417 try:
424 try:
418 log.debug("Trying to open URL %s", cleaned_uri)
425 log.debug("Trying to open URL %s", cleaned_uri)
419 resp = o.open(req)
426 resp = o.open(req)
420 if resp.code != 200:
427 if resp.code != 200:
421 raise exceptions.URLError()('Return Code is not 200')
428 raise exceptions.URLError()('Return Code is not 200')
422 except Exception as e:
429 except Exception as e:
423 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
430 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
424 # means it cannot be cloned
431 # means it cannot be cloned
425 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
432 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
426
433
427 # now check if it's a proper hg repo, but don't do it for svn
434 # now check if it's a proper hg repo, but don't do it for svn
428 try:
435 try:
429 if _proto == 'svn':
436 if _proto == 'svn':
430 pass
437 pass
431 else:
438 else:
432 # check for pure hg repos
439 # check for pure hg repos
433 log.debug(
440 log.debug(
434 "Verifying if URL is a Mercurial repository: %s",
441 "Verifying if URL is a Mercurial repository: %s",
435 cleaned_uri)
442 cleaned_uri)
436 ui = make_ui_from_config(config)
443 ui = make_ui_from_config(config)
437 peer_checker = makepeer(ui, url)
444 peer_checker = makepeer(ui, url)
438 peer_checker.lookup('tip')
445 peer_checker.lookup('tip')
439 except Exception as e:
446 except Exception as e:
440 log.warning("URL is not a valid Mercurial repository: %s",
447 log.warning("URL is not a valid Mercurial repository: %s",
441 cleaned_uri)
448 cleaned_uri)
442 raise exceptions.URLError(e)(
449 raise exceptions.URLError(e)(
443 "url [%s] does not look like an hg repo org_exc: %s"
450 "url [%s] does not look like an hg repo org_exc: %s"
444 % (cleaned_uri, e))
451 % (cleaned_uri, e))
445
452
446 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
453 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
447 return True
454 return True
448
455
449 @reraise_safe_exceptions
456 @reraise_safe_exceptions
450 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
457 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
451 repo = self._factory.repo(wire)
458 repo = self._factory.repo(wire)
452
459
453 if file_filter:
460 if file_filter:
454 match_filter = match(file_filter[0], '', [file_filter[1]])
461 match_filter = match(file_filter[0], '', [file_filter[1]])
455 else:
462 else:
456 match_filter = file_filter
463 match_filter = file_filter
457 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
464 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
458
465
459 try:
466 try:
460 return "".join(patch.diff(
467 return "".join(patch.diff(
461 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts))
468 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts))
462 except RepoLookupError as e:
469 except RepoLookupError as e:
463 raise exceptions.LookupException(e)()
470 raise exceptions.LookupException(e)()
464
471
465 @reraise_safe_exceptions
472 @reraise_safe_exceptions
466 def node_history(self, wire, revision, path, limit):
473 def node_history(self, wire, revision, path, limit):
467 cache_on, context_uid, repo_id = self._cache_on(wire)
474 cache_on, context_uid, repo_id = self._cache_on(wire)
468 region = self._region(wire)
475 region = self._region(wire)
476
469 @region.conditional_cache_on_arguments(condition=cache_on)
477 @region.conditional_cache_on_arguments(condition=cache_on)
470 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
478 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
471 repo = self._factory.repo(wire)
479 repo = self._factory.repo(wire)
472
480
473 ctx = self._get_ctx(repo, revision)
481 ctx = self._get_ctx(repo, revision)
474 fctx = ctx.filectx(path)
482 fctx = ctx.filectx(path)
475
483
476 def history_iter():
484 def history_iter():
477 limit_rev = fctx.rev()
485 limit_rev = fctx.rev()
478 for obj in reversed(list(fctx.filelog())):
486 for obj in reversed(list(fctx.filelog())):
479 obj = fctx.filectx(obj)
487 obj = fctx.filectx(obj)
480 ctx = obj.changectx()
488 ctx = obj.changectx()
481 if ctx.hidden() or ctx.obsolete():
489 if ctx.hidden() or ctx.obsolete():
482 continue
490 continue
483
491
484 if limit_rev >= obj.rev():
492 if limit_rev >= obj.rev():
485 yield obj
493 yield obj
486
494
487 history = []
495 history = []
488 for cnt, obj in enumerate(history_iter()):
496 for cnt, obj in enumerate(history_iter()):
489 if limit and cnt >= limit:
497 if limit and cnt >= limit:
490 break
498 break
491 history.append(hex(obj.node()))
499 history.append(hex(obj.node()))
492
500
493 return [x for x in history]
501 return [x for x in history]
494 return _node_history(context_uid, repo_id, revision, path, limit)
502 return _node_history(context_uid, repo_id, revision, path, limit)
495
503
496 @reraise_safe_exceptions
504 @reraise_safe_exceptions
497 def node_history_untill(self, wire, revision, path, limit):
505 def node_history_untill(self, wire, revision, path, limit):
498 cache_on, context_uid, repo_id = self._cache_on(wire)
506 cache_on, context_uid, repo_id = self._cache_on(wire)
499 region = self._region(wire)
507 region = self._region(wire)
508
500 @region.conditional_cache_on_arguments(condition=cache_on)
509 @region.conditional_cache_on_arguments(condition=cache_on)
501 def _node_history_until(_context_uid, _repo_id):
510 def _node_history_until(_context_uid, _repo_id):
502 repo = self._factory.repo(wire)
511 repo = self._factory.repo(wire)
503 ctx = self._get_ctx(repo, revision)
512 ctx = self._get_ctx(repo, revision)
504 fctx = ctx.filectx(path)
513 fctx = ctx.filectx(path)
505
514
506 file_log = list(fctx.filelog())
515 file_log = list(fctx.filelog())
507 if limit:
516 if limit:
508 # Limit to the last n items
517 # Limit to the last n items
509 file_log = file_log[-limit:]
518 file_log = file_log[-limit:]
510
519
511 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
520 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
512 return _node_history_until(context_uid, repo_id, revision, path, limit)
521 return _node_history_until(context_uid, repo_id, revision, path, limit)
513
522
514 @reraise_safe_exceptions
523 @reraise_safe_exceptions
515 def fctx_annotate(self, wire, revision, path):
524 def fctx_annotate(self, wire, revision, path):
516 repo = self._factory.repo(wire)
525 repo = self._factory.repo(wire)
517 ctx = self._get_ctx(repo, revision)
526 ctx = self._get_ctx(repo, revision)
518 fctx = ctx.filectx(path)
527 fctx = ctx.filectx(path)
519
528
520 result = []
529 result = []
521 for i, annotate_obj in enumerate(fctx.annotate(), 1):
530 for i, annotate_obj in enumerate(fctx.annotate(), 1):
522 ln_no = i
531 ln_no = i
523 sha = hex(annotate_obj.fctx.node())
532 sha = hex(annotate_obj.fctx.node())
524 content = annotate_obj.text
533 content = annotate_obj.text
525 result.append((ln_no, sha, content))
534 result.append((ln_no, sha, content))
526 return result
535 return result
527
536
528 @reraise_safe_exceptions
537 @reraise_safe_exceptions
529 def fctx_node_data(self, wire, revision, path):
538 def fctx_node_data(self, wire, revision, path):
530 repo = self._factory.repo(wire)
539 repo = self._factory.repo(wire)
531 ctx = self._get_ctx(repo, revision)
540 ctx = self._get_ctx(repo, revision)
532 fctx = ctx.filectx(path)
541 fctx = ctx.filectx(path)
533 return fctx.data()
542 return fctx.data_queue()
534
543
535 @reraise_safe_exceptions
544 @reraise_safe_exceptions
536 def fctx_flags(self, wire, commit_id, path):
545 def fctx_flags(self, wire, commit_id, path):
537 cache_on, context_uid, repo_id = self._cache_on(wire)
546 cache_on, context_uid, repo_id = self._cache_on(wire)
538 region = self._region(wire)
547 region = self._region(wire)
539 @region.conditional_cache_on_arguments(condition=cache_on)
548 @region.conditional_cache_on_arguments(condition=cache_on)
540 def _fctx_flags(_repo_id, _commit_id, _path):
549 def _fctx_flags(_repo_id, _commit_id, _path):
541 repo = self._factory.repo(wire)
550 repo = self._factory.repo(wire)
542 ctx = self._get_ctx(repo, commit_id)
551 ctx = self._get_ctx(repo, commit_id)
543 fctx = ctx.filectx(path)
552 fctx = ctx.filectx(path)
544 return fctx.flags()
553 return fctx.flags()
545
554
546 return _fctx_flags(repo_id, commit_id, path)
555 return _fctx_flags(repo_id, commit_id, path)
547
556
548 @reraise_safe_exceptions
557 @reraise_safe_exceptions
549 def fctx_size(self, wire, commit_id, path):
558 def fctx_size(self, wire, commit_id, path):
550 cache_on, context_uid, repo_id = self._cache_on(wire)
559 cache_on, context_uid, repo_id = self._cache_on(wire)
551 region = self._region(wire)
560 region = self._region(wire)
552 @region.conditional_cache_on_arguments(condition=cache_on)
561 @region.conditional_cache_on_arguments(condition=cache_on)
553 def _fctx_size(_repo_id, _revision, _path):
562 def _fctx_size(_repo_id, _revision, _path):
554 repo = self._factory.repo(wire)
563 repo = self._factory.repo(wire)
555 ctx = self._get_ctx(repo, commit_id)
564 ctx = self._get_ctx(repo, commit_id)
556 fctx = ctx.filectx(path)
565 fctx = ctx.filectx(path)
557 return fctx.size()
566 return fctx.size()
558 return _fctx_size(repo_id, commit_id, path)
567 return _fctx_size(repo_id, commit_id, path)
559
568
560 @reraise_safe_exceptions
569 @reraise_safe_exceptions
561 def get_all_commit_ids(self, wire, name):
570 def get_all_commit_ids(self, wire, name):
562 cache_on, context_uid, repo_id = self._cache_on(wire)
571 cache_on, context_uid, repo_id = self._cache_on(wire)
563 region = self._region(wire)
572 region = self._region(wire)
573
564 @region.conditional_cache_on_arguments(condition=cache_on)
574 @region.conditional_cache_on_arguments(condition=cache_on)
565 def _get_all_commit_ids(_context_uid, _repo_id, _name):
575 def _get_all_commit_ids(_context_uid, _repo_id, _name):
566 repo = self._factory.repo(wire)
576 repo = self._factory.repo(wire)
567 repo = repo.filtered(name)
577 repo = repo.filtered(name)
568 revs = [hex(x[7]) for x in repo.changelog.index]
578 revs = [ascii_str(repo[x].hex()) for x in repo.filtered(b'visible').changelog.revs()]
569 return revs
579 return revs
570 return _get_all_commit_ids(context_uid, repo_id, name)
580 return _get_all_commit_ids(context_uid, repo_id, name)
571
581
572 @reraise_safe_exceptions
582 @reraise_safe_exceptions
573 def get_config_value(self, wire, section, name, untrusted=False):
583 def get_config_value(self, wire, section, name, untrusted=False):
574 repo = self._factory.repo(wire)
584 repo = self._factory.repo(wire)
575 return repo.ui.config(section, name, untrusted=untrusted)
585 return repo.ui.config(section, name, untrusted=untrusted)
576
586
577 @reraise_safe_exceptions
587 @reraise_safe_exceptions
578 def is_large_file(self, wire, commit_id, path):
588 def is_large_file(self, wire, commit_id, path):
579 cache_on, context_uid, repo_id = self._cache_on(wire)
589 cache_on, context_uid, repo_id = self._cache_on(wire)
580 region = self._region(wire)
590 region = self._region(wire)
591
581 @region.conditional_cache_on_arguments(condition=cache_on)
592 @region.conditional_cache_on_arguments(condition=cache_on)
582 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
593 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
583 return largefiles.lfutil.isstandin(path)
594 return largefiles.lfutil.isstandin(path)
584
595
585 return _is_large_file(context_uid, repo_id, commit_id, path)
596 return _is_large_file(context_uid, repo_id, commit_id, path)
586
597
587 @reraise_safe_exceptions
598 @reraise_safe_exceptions
588 def is_binary(self, wire, revision, path):
599 def is_binary(self, wire, revision, path):
589 cache_on, context_uid, repo_id = self._cache_on(wire)
600 cache_on, context_uid, repo_id = self._cache_on(wire)
601 region = self._region(wire)
590
602
591 region = self._region(wire)
592 @region.conditional_cache_on_arguments(condition=cache_on)
603 @region.conditional_cache_on_arguments(condition=cache_on)
593 def _is_binary(_repo_id, _sha, _path):
604 def _is_binary(_repo_id, _sha, _path):
594 repo = self._factory.repo(wire)
605 repo = self._factory.repo(wire)
595 ctx = self._get_ctx(repo, revision)
606 ctx = self._get_ctx(repo, revision)
596 fctx = ctx.filectx(path)
607 fctx = ctx.filectx(path)
597 return fctx.isbinary()
608 return fctx.isbinary()
598
609
599 return _is_binary(repo_id, revision, path)
610 return _is_binary(repo_id, revision, path)
600
611
601 @reraise_safe_exceptions
612 @reraise_safe_exceptions
602 def in_largefiles_store(self, wire, sha):
613 def in_largefiles_store(self, wire, sha):
603 repo = self._factory.repo(wire)
614 repo = self._factory.repo(wire)
604 return largefiles.lfutil.instore(repo, sha)
615 return largefiles.lfutil.instore(repo, sha)
605
616
606 @reraise_safe_exceptions
617 @reraise_safe_exceptions
607 def in_user_cache(self, wire, sha):
618 def in_user_cache(self, wire, sha):
608 repo = self._factory.repo(wire)
619 repo = self._factory.repo(wire)
609 return largefiles.lfutil.inusercache(repo.ui, sha)
620 return largefiles.lfutil.inusercache(repo.ui, sha)
610
621
611 @reraise_safe_exceptions
622 @reraise_safe_exceptions
612 def store_path(self, wire, sha):
623 def store_path(self, wire, sha):
613 repo = self._factory.repo(wire)
624 repo = self._factory.repo(wire)
614 return largefiles.lfutil.storepath(repo, sha)
625 return largefiles.lfutil.storepath(repo, sha)
615
626
616 @reraise_safe_exceptions
627 @reraise_safe_exceptions
617 def link(self, wire, sha, path):
628 def link(self, wire, sha, path):
618 repo = self._factory.repo(wire)
629 repo = self._factory.repo(wire)
619 largefiles.lfutil.link(
630 largefiles.lfutil.link(
620 largefiles.lfutil.usercachepath(repo.ui, sha), path)
631 largefiles.lfutil.usercachepath(repo.ui, sha), path)
621
632
622 @reraise_safe_exceptions
633 @reraise_safe_exceptions
623 def localrepository(self, wire, create=False):
634 def localrepository(self, wire, create=False):
624 self._factory.repo(wire, create=create)
635 self._factory.repo(wire, create=create)
625
636
626 @reraise_safe_exceptions
637 @reraise_safe_exceptions
627 def lookup(self, wire, revision, both):
638 def lookup(self, wire, revision, both):
628 cache_on, context_uid, repo_id = self._cache_on(wire)
639 cache_on, context_uid, repo_id = self._cache_on(wire)
629
640
630 region = self._region(wire)
641 region = self._region(wire)
631 @region.conditional_cache_on_arguments(condition=cache_on)
642 @region.conditional_cache_on_arguments(condition=cache_on)
632 def _lookup(_context_uid, _repo_id, _revision, _both):
643 def _lookup(_context_uid, _repo_id, _revision, _both):
633
644
634 repo = self._factory.repo(wire)
645 repo = self._factory.repo(wire)
635 rev = _revision
646 rev = _revision
636 if isinstance(rev, int):
647 if isinstance(rev, int):
637 # NOTE(marcink):
648 # NOTE(marcink):
638 # since Mercurial doesn't support negative indexes properly
649 # since Mercurial doesn't support negative indexes properly
639 # we need to shift accordingly by one to get proper index, e.g
650 # we need to shift accordingly by one to get proper index, e.g
640 # repo[-1] => repo[-2]
651 # repo[-1] => repo[-2]
641 # repo[0] => repo[-1]
652 # repo[0] => repo[-1]
642 if rev <= 0:
653 if rev <= 0:
643 rev = rev + -1
654 rev = rev + -1
644 try:
655 try:
645 ctx = self._get_ctx(repo, rev)
656 ctx = self._get_ctx(repo, rev)
646 except (TypeError, RepoLookupError) as e:
657 except (TypeError, RepoLookupError) as e:
647 e._org_exc_tb = traceback.format_exc()
658 e._org_exc_tb = traceback.format_exc()
648 raise exceptions.LookupException(e)(rev)
659 raise exceptions.LookupException(e)(rev)
649 except LookupError as e:
660 except LookupError as e:
650 e._org_exc_tb = traceback.format_exc()
661 e._org_exc_tb = traceback.format_exc()
651 raise exceptions.LookupException(e)(e.name)
662 raise exceptions.LookupException(e)(e.name)
652
663
653 if not both:
664 if not both:
654 return ctx.hex()
665 return ctx.hex()
655
666
656 ctx = repo[ctx.hex()]
667 ctx = repo[ctx.hex()]
657 return ctx.hex(), ctx.rev()
668 return ctx.hex(), ctx.rev()
658
669
659 return _lookup(context_uid, repo_id, revision, both)
670 return _lookup(context_uid, repo_id, revision, both)
660
671
661 @reraise_safe_exceptions
672 @reraise_safe_exceptions
662 def sync_push(self, wire, url):
673 def sync_push(self, wire, url):
663 if not self.check_url(url, wire['config']):
674 if not self.check_url(url, wire['config']):
664 return
675 return
665
676
666 repo = self._factory.repo(wire)
677 repo = self._factory.repo(wire)
667
678
668 # Disable any prompts for this repo
679 # Disable any prompts for this repo
669 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
680 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
670
681
671 bookmarks = list(dict(repo._bookmarks).keys())
682 bookmarks = list(dict(repo._bookmarks).keys())
672 remote = peer(repo, {}, url)
683 remote = peer(repo, {}, url)
673 # Disable any prompts for this remote
684 # Disable any prompts for this remote
674 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
685 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
675
686
676 return exchange.push(
687 return exchange.push(
677 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
688 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
678
689
679 @reraise_safe_exceptions
690 @reraise_safe_exceptions
680 def revision(self, wire, rev):
691 def revision(self, wire, rev):
681 repo = self._factory.repo(wire)
692 repo = self._factory.repo(wire)
682 ctx = self._get_ctx(repo, rev)
693 ctx = self._get_ctx(repo, rev)
683 return ctx.rev()
694 return ctx.rev()
684
695
685 @reraise_safe_exceptions
696 @reraise_safe_exceptions
686 def rev_range(self, wire, commit_filter):
697 def rev_range(self, wire, commit_filter):
687 cache_on, context_uid, repo_id = self._cache_on(wire)
698 cache_on, context_uid, repo_id = self._cache_on(wire)
688
699
689 region = self._region(wire)
700 region = self._region(wire)
690 @region.conditional_cache_on_arguments(condition=cache_on)
701 @region.conditional_cache_on_arguments(condition=cache_on)
691 def _rev_range(_context_uid, _repo_id, _filter):
702 def _rev_range(_context_uid, _repo_id, _filter):
692 repo = self._factory.repo(wire)
703 repo = self._factory.repo(wire)
693 revisions = [rev for rev in revrange(repo, commit_filter)]
704 revisions = [rev for rev in revrange(repo, commit_filter)]
694 return revisions
705 return revisions
695
706
696 return _rev_range(context_uid, repo_id, sorted(commit_filter))
707 return _rev_range(context_uid, repo_id, sorted(commit_filter))
697
708
698 @reraise_safe_exceptions
709 @reraise_safe_exceptions
699 def rev_range_hash(self, wire, node):
710 def rev_range_hash(self, wire, node):
700 repo = self._factory.repo(wire)
711 repo = self._factory.repo(wire)
701
712
702 def get_revs(repo, rev_opt):
713 def get_revs(repo, rev_opt):
703 if rev_opt:
714 if rev_opt:
704 revs = revrange(repo, rev_opt)
715 revs = revrange(repo, rev_opt)
705 if len(revs) == 0:
716 if len(revs) == 0:
706 return (nullrev, nullrev)
717 return (nullrev, nullrev)
707 return max(revs), min(revs)
718 return max(revs), min(revs)
708 else:
719 else:
709 return len(repo) - 1, 0
720 return len(repo) - 1, 0
710
721
711 stop, start = get_revs(repo, [node + ':'])
722 stop, start = get_revs(repo, [node + ':'])
712 revs = [hex(repo[r].node()) for r in range(start, stop + 1)]
723 revs = [hex(repo[r].node()) for r in range(start, stop + 1)]
713 return revs
724 return revs
714
725
715 @reraise_safe_exceptions
726 @reraise_safe_exceptions
716 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
727 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
717 other_path = kwargs.pop('other_path', None)
728 other_path = kwargs.pop('other_path', None)
718
729
719 # case when we want to compare two independent repositories
730 # case when we want to compare two independent repositories
720 if other_path and other_path != wire["path"]:
731 if other_path and other_path != wire["path"]:
721 baseui = self._factory._create_config(wire["config"])
732 baseui = self._factory._create_config(wire["config"])
722 repo = unionrepo.makeunionrepository(baseui, other_path, wire["path"])
733 repo = unionrepo.makeunionrepository(baseui, other_path, wire["path"])
723 else:
734 else:
724 repo = self._factory.repo(wire)
735 repo = self._factory.repo(wire)
725 return list(repo.revs(rev_spec, *args))
736 return list(repo.revs(rev_spec, *args))
726
737
727 @reraise_safe_exceptions
738 @reraise_safe_exceptions
728 def verify(self, wire,):
739 def verify(self, wire,):
729 repo = self._factory.repo(wire)
740 repo = self._factory.repo(wire)
730 baseui = self._factory._create_config(wire['config'])
741 baseui = self._factory._create_config(wire['config'])
731
742
732 baseui, output = patch_ui_message_output(baseui)
743 baseui, output = patch_ui_message_output(baseui)
733
744
734 repo.ui = baseui
745 repo.ui = baseui
735 verify.verify(repo)
746 verify.verify(repo)
736 return output.getvalue()
747 return output.getvalue()
737
748
738 @reraise_safe_exceptions
749 @reraise_safe_exceptions
739 def hg_update_cache(self, wire,):
750 def hg_update_cache(self, wire,):
740 repo = self._factory.repo(wire)
751 repo = self._factory.repo(wire)
741 baseui = self._factory._create_config(wire['config'])
752 baseui = self._factory._create_config(wire['config'])
742 baseui, output = patch_ui_message_output(baseui)
753 baseui, output = patch_ui_message_output(baseui)
743
754
744 repo.ui = baseui
755 repo.ui = baseui
745 with repo.wlock(), repo.lock():
756 with repo.wlock(), repo.lock():
746 repo.updatecaches(full=True)
757 repo.updatecaches(full=True)
747
758
748 return output.getvalue()
759 return output.getvalue()
749
760
750 @reraise_safe_exceptions
761 @reraise_safe_exceptions
751 def hg_rebuild_fn_cache(self, wire,):
762 def hg_rebuild_fn_cache(self, wire,):
752 repo = self._factory.repo(wire)
763 repo = self._factory.repo(wire)
753 baseui = self._factory._create_config(wire['config'])
764 baseui = self._factory._create_config(wire['config'])
754 baseui, output = patch_ui_message_output(baseui)
765 baseui, output = patch_ui_message_output(baseui)
755
766
756 repo.ui = baseui
767 repo.ui = baseui
757
768
758 repair.rebuildfncache(baseui, repo)
769 repair.rebuildfncache(baseui, repo)
759
770
760 return output.getvalue()
771 return output.getvalue()
761
772
762 @reraise_safe_exceptions
773 @reraise_safe_exceptions
763 def tags(self, wire):
774 def tags(self, wire):
764 cache_on, context_uid, repo_id = self._cache_on(wire)
775 cache_on, context_uid, repo_id = self._cache_on(wire)
765 region = self._region(wire)
776 region = self._region(wire)
766 @region.conditional_cache_on_arguments(condition=cache_on)
777 @region.conditional_cache_on_arguments(condition=cache_on)
767 def _tags(_context_uid, _repo_id):
778 def _tags(_context_uid, _repo_id):
768 repo = self._factory.repo(wire)
779 repo = self._factory.repo(wire)
769 return repo.tags()
780 return repo.tags()
770
781
771 return _tags(context_uid, repo_id)
782 return _tags(context_uid, repo_id)
772
783
773 @reraise_safe_exceptions
784 @reraise_safe_exceptions
774 def update(self, wire, node=None, clean=False):
785 def update(self, wire, node=None, clean=False):
775 repo = self._factory.repo(wire)
786 repo = self._factory.repo(wire)
776 baseui = self._factory._create_config(wire['config'])
787 baseui = self._factory._create_config(wire['config'])
777 commands.update(baseui, repo, node=node, clean=clean)
788 commands.update(baseui, repo, node=node, clean=clean)
778
789
779 @reraise_safe_exceptions
790 @reraise_safe_exceptions
780 def identify(self, wire):
791 def identify(self, wire):
781 repo = self._factory.repo(wire)
792 repo = self._factory.repo(wire)
782 baseui = self._factory._create_config(wire['config'])
793 baseui = self._factory._create_config(wire['config'])
783 output = io.BytesIO()
794 output = io.BytesIO()
784 baseui.write = output.write
795 baseui.write = output.write
785 # This is required to get a full node id
796 # This is required to get a full node id
786 baseui.debugflag = True
797 baseui.debugflag = True
787 commands.identify(baseui, repo, id=True)
798 commands.identify(baseui, repo, id=True)
788
799
789 return output.getvalue()
800 return output.getvalue()
790
801
791 @reraise_safe_exceptions
802 @reraise_safe_exceptions
792 def heads(self, wire, branch=None):
803 def heads(self, wire, branch=None):
793 repo = self._factory.repo(wire)
804 repo = self._factory.repo(wire)
794 baseui = self._factory._create_config(wire['config'])
805 baseui = self._factory._create_config(wire['config'])
795 output = io.BytesIO()
806 output = io.BytesIO()
796
807
797 def write(data, **unused_kwargs):
808 def write(data, **unused_kwargs):
798 output.write(data)
809 output.write(data)
799
810
800 baseui.write = write
811 baseui.write = write
801 if branch:
812 if branch:
802 args = [branch]
813 args = [branch]
803 else:
814 else:
804 args = []
815 args = []
805 commands.heads(baseui, repo, template='{node} ', *args)
816 commands.heads(baseui, repo, template='{node} ', *args)
806
817
807 return output.getvalue()
818 return output.getvalue()
808
819
809 @reraise_safe_exceptions
820 @reraise_safe_exceptions
810 def ancestor(self, wire, revision1, revision2):
821 def ancestor(self, wire, revision1, revision2):
811 repo = self._factory.repo(wire)
822 repo = self._factory.repo(wire)
812 changelog = repo.changelog
823 changelog = repo.changelog
813 lookup = repo.lookup
824 lookup = repo.lookup
814 a = changelog.ancestor(lookup(revision1), lookup(revision2))
825 a = changelog.ancestor(lookup(revision1), lookup(revision2))
815 return hex(a)
826 return hex(a)
816
827
817 @reraise_safe_exceptions
828 @reraise_safe_exceptions
818 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
829 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
819 baseui = self._factory._create_config(wire["config"], hooks=hooks)
830 baseui = self._factory._create_config(wire["config"], hooks=hooks)
820 clone(baseui, source, dest, noupdate=not update_after_clone)
831 clone(baseui, source, dest, noupdate=not update_after_clone)
821
832
822 @reraise_safe_exceptions
833 @reraise_safe_exceptions
823 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
834 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
824
835
825 repo = self._factory.repo(wire)
836 repo = self._factory.repo(wire)
826 baseui = self._factory._create_config(wire['config'])
837 baseui = self._factory._create_config(wire['config'])
827 publishing = baseui.configbool('phases', 'publish')
838 publishing = baseui.configbool('phases', 'publish')
828 if publishing:
839 if publishing:
829 new_commit = 'public'
840 new_commit = 'public'
830 else:
841 else:
831 new_commit = 'draft'
842 new_commit = 'draft'
832
843
833 def _filectxfn(_repo, ctx, path):
844 def _filectxfn(_repo, ctx, path):
834 """
845 """
835 Marks given path as added/changed/removed in a given _repo. This is
846 Marks given path as added/changed/removed in a given _repo. This is
836 for internal mercurial commit function.
847 for internal mercurial commit function.
837 """
848 """
838
849
839 # check if this path is removed
850 # check if this path is removed
840 if path in removed:
851 if path in removed:
841 # returning None is a way to mark node for removal
852 # returning None is a way to mark node for removal
842 return None
853 return None
843
854
844 # check if this path is added
855 # check if this path is added
845 for node in updated:
856 for node in updated:
846 if node['path'] == path:
857 if node['path'] == path:
847 return memfilectx(
858 return memfilectx(
848 _repo,
859 _repo,
849 changectx=ctx,
860 changectx=ctx,
850 path=node['path'],
861 path=node['path'],
851 data=node['content'],
862 data=node['content'],
852 islink=False,
863 islink=False,
853 isexec=bool(node['mode'] & stat.S_IXUSR),
864 isexec=bool(node['mode'] & stat.S_IXUSR),
854 copysource=False)
865 copysource=False)
855
866
856 raise exceptions.AbortException()(
867 raise exceptions.AbortException()(
857 "Given path haven't been marked as added, "
868 "Given path haven't been marked as added, "
858 "changed or removed (%s)" % path)
869 "changed or removed (%s)" % path)
859
870
860 with repo.ui.configoverride({('phases', 'new-commit'): new_commit}):
871 with repo.ui.configoverride({('phases', 'new-commit'): new_commit}):
861
872
862 commit_ctx = memctx(
873 commit_ctx = memctx(
863 repo=repo,
874 repo=repo,
864 parents=parents,
875 parents=parents,
865 text=message,
876 text=message,
866 files=files,
877 files=files,
867 filectxfn=_filectxfn,
878 filectxfn=_filectxfn,
868 user=user,
879 user=user,
869 date=(commit_time, commit_timezone),
880 date=(commit_time, commit_timezone),
870 extra=extra)
881 extra=extra)
871
882
872 n = repo.commitctx(commit_ctx)
883 n = repo.commitctx(commit_ctx)
873 new_id = hex(n)
884 new_id = hex(n)
874
885
875 return new_id
886 return new_id
876
887
877 @reraise_safe_exceptions
888 @reraise_safe_exceptions
878 def pull(self, wire, url, commit_ids=None):
889 def pull(self, wire, url, commit_ids=None):
879 repo = self._factory.repo(wire)
890 repo = self._factory.repo(wire)
880 # Disable any prompts for this repo
891 # Disable any prompts for this repo
881 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
892 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
882
893
883 remote = peer(repo, {}, url)
894 remote = peer(repo, {}, url)
884 # Disable any prompts for this remote
895 # Disable any prompts for this remote
885 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
896 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
886
897
887 if commit_ids:
898 if commit_ids:
888 commit_ids = [bin(commit_id) for commit_id in commit_ids]
899 commit_ids = [bin(commit_id) for commit_id in commit_ids]
889
900
890 return exchange.pull(
901 return exchange.pull(
891 repo, remote, heads=commit_ids, force=None).cgresult
902 repo, remote, heads=commit_ids, force=None).cgresult
892
903
893 @reraise_safe_exceptions
904 @reraise_safe_exceptions
894 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None, hooks=True):
905 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None, hooks=True):
895 repo = self._factory.repo(wire)
906 repo = self._factory.repo(wire)
896 baseui = self._factory._create_config(wire['config'], hooks=hooks)
907 baseui = self._factory._create_config(wire['config'], hooks=hooks)
897
908
898 # Mercurial internally has a lot of logic that checks ONLY if
909 # Mercurial internally has a lot of logic that checks ONLY if
899 # option is defined, we just pass those if they are defined then
910 # option is defined, we just pass those if they are defined then
900 opts = {}
911 opts = {}
901 if bookmark:
912 if bookmark:
902 opts['bookmark'] = bookmark
913 opts['bookmark'] = bookmark
903 if branch:
914 if branch:
904 opts['branch'] = branch
915 opts['branch'] = branch
905 if revision:
916 if revision:
906 opts['rev'] = revision
917 opts['rev'] = revision
907
918
908 commands.pull(baseui, repo, source, **opts)
919 commands.pull(baseui, repo, source, **opts)
909
920
910 @reraise_safe_exceptions
921 @reraise_safe_exceptions
911 def push(self, wire, revisions, dest_path, hooks=True, push_branches=False):
922 def push(self, wire, revisions, dest_path, hooks=True, push_branches=False):
912 repo = self._factory.repo(wire)
923 repo = self._factory.repo(wire)
913 baseui = self._factory._create_config(wire['config'], hooks=hooks)
924 baseui = self._factory._create_config(wire['config'], hooks=hooks)
914 commands.push(baseui, repo, dest=dest_path, rev=revisions,
925 commands.push(baseui, repo, dest=dest_path, rev=revisions,
915 new_branch=push_branches)
926 new_branch=push_branches)
916
927
917 @reraise_safe_exceptions
928 @reraise_safe_exceptions
918 def strip(self, wire, revision, update, backup):
929 def strip(self, wire, revision, update, backup):
919 repo = self._factory.repo(wire)
930 repo = self._factory.repo(wire)
920 ctx = self._get_ctx(repo, revision)
931 ctx = self._get_ctx(repo, revision)
921 hgext_strip(
932 hgext_strip(
922 repo.baseui, repo, ctx.node(), update=update, backup=backup)
933 repo.baseui, repo, ctx.node(), update=update, backup=backup)
923
934
924 @reraise_safe_exceptions
935 @reraise_safe_exceptions
925 def get_unresolved_files(self, wire):
936 def get_unresolved_files(self, wire):
926 repo = self._factory.repo(wire)
937 repo = self._factory.repo(wire)
927
938
928 log.debug('Calculating unresolved files for repo: %s', repo)
939 log.debug('Calculating unresolved files for repo: %s', repo)
929 output = io.BytesIO()
940 output = io.BytesIO()
930
941
931 def write(data, **unused_kwargs):
942 def write(data, **unused_kwargs):
932 output.write(data)
943 output.write(data)
933
944
934 baseui = self._factory._create_config(wire['config'])
945 baseui = self._factory._create_config(wire['config'])
935 baseui.write = write
946 baseui.write = write
936
947
937 commands.resolve(baseui, repo, list=True)
948 commands.resolve(baseui, repo, list=True)
938 unresolved = output.getvalue().splitlines(0)
949 unresolved = output.getvalue().splitlines(0)
939 return unresolved
950 return unresolved
940
951
941 @reraise_safe_exceptions
952 @reraise_safe_exceptions
942 def merge(self, wire, revision):
953 def merge(self, wire, revision):
943 repo = self._factory.repo(wire)
954 repo = self._factory.repo(wire)
944 baseui = self._factory._create_config(wire['config'])
955 baseui = self._factory._create_config(wire['config'])
945 repo.ui.setconfig('ui', 'merge', 'internal:dump')
956 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
946
957
947 # In case of sub repositories are used mercurial prompts the user in
958 # In case of sub repositories are used mercurial prompts the user in
948 # case of merge conflicts or different sub repository sources. By
959 # case of merge conflicts or different sub repository sources. By
949 # setting the interactive flag to `False` mercurial doesn't prompt the
960 # setting the interactive flag to `False` mercurial doesn't prompt the
950 # used but instead uses a default value.
961 # used but instead uses a default value.
951 repo.ui.setconfig('ui', 'interactive', False)
962 repo.ui.setconfig(b'ui', b'interactive', False)
952 commands.merge(baseui, repo, rev=revision)
963 commands.merge(baseui, repo, rev=revision)
953
964
954 @reraise_safe_exceptions
965 @reraise_safe_exceptions
955 def merge_state(self, wire):
966 def merge_state(self, wire):
956 repo = self._factory.repo(wire)
967 repo = self._factory.repo(wire)
957 repo.ui.setconfig('ui', 'merge', 'internal:dump')
968 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
958
969
959 # In case of sub repositories are used mercurial prompts the user in
970 # In case of sub repositories are used mercurial prompts the user in
960 # case of merge conflicts or different sub repository sources. By
971 # case of merge conflicts or different sub repository sources. By
961 # setting the interactive flag to `False` mercurial doesn't prompt the
972 # setting the interactive flag to `False` mercurial doesn't prompt the
962 # used but instead uses a default value.
973 # used but instead uses a default value.
963 repo.ui.setconfig('ui', 'interactive', False)
974 repo.ui.setconfig(b'ui', b'interactive', False)
964 ms = hg_merge.mergestate(repo)
975 ms = hg_merge.mergestate(repo)
965 return [x for x in ms.unresolved()]
976 return [x for x in ms.unresolved()]
966
977
967 @reraise_safe_exceptions
978 @reraise_safe_exceptions
968 def commit(self, wire, message, username, close_branch=False):
979 def commit(self, wire, message, username, close_branch=False):
969 repo = self._factory.repo(wire)
980 repo = self._factory.repo(wire)
970 baseui = self._factory._create_config(wire['config'])
981 baseui = self._factory._create_config(wire['config'])
971 repo.ui.setconfig('ui', 'username', username)
982 repo.ui.setconfig(b'ui', b'username', username)
972 commands.commit(baseui, repo, message=message, close_branch=close_branch)
983 commands.commit(baseui, repo, message=message, close_branch=close_branch)
973
984
974 @reraise_safe_exceptions
985 @reraise_safe_exceptions
975 def rebase(self, wire, source=None, dest=None, abort=False):
986 def rebase(self, wire, source=None, dest=None, abort=False):
976 repo = self._factory.repo(wire)
987 repo = self._factory.repo(wire)
977 baseui = self._factory._create_config(wire['config'])
988 baseui = self._factory._create_config(wire['config'])
978 repo.ui.setconfig('ui', 'merge', 'internal:dump')
989 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
979 # In case of sub repositories are used mercurial prompts the user in
990 # In case of sub repositories are used mercurial prompts the user in
980 # case of merge conflicts or different sub repository sources. By
991 # case of merge conflicts or different sub repository sources. By
981 # setting the interactive flag to `False` mercurial doesn't prompt the
992 # setting the interactive flag to `False` mercurial doesn't prompt the
982 # used but instead uses a default value.
993 # used but instead uses a default value.
983 repo.ui.setconfig('ui', 'interactive', False)
994 repo.ui.setconfig(b'ui', b'interactive', False)
984 rebase.rebase(baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
995 rebase.rebase(baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
985
996
986 @reraise_safe_exceptions
997 @reraise_safe_exceptions
987 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
998 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
988 repo = self._factory.repo(wire)
999 repo = self._factory.repo(wire)
989 ctx = self._get_ctx(repo, revision)
1000 ctx = self._get_ctx(repo, revision)
990 node = ctx.node()
1001 node = ctx.node()
991
1002
992 date = (tag_time, tag_timezone)
1003 date = (tag_time, tag_timezone)
993 try:
1004 try:
994 hg_tag.tag(repo, name, node, message, local, user, date)
1005 hg_tag.tag(repo, name, node, message, local, user, date)
995 except Abort as e:
1006 except Abort as e:
996 log.exception("Tag operation aborted")
1007 log.exception("Tag operation aborted")
997 # Exception can contain unicode which we convert
1008 # Exception can contain unicode which we convert
998 raise exceptions.AbortException(e)(repr(e))
1009 raise exceptions.AbortException(e)(repr(e))
999
1010
1000 @reraise_safe_exceptions
1011 @reraise_safe_exceptions
1001 def bookmark(self, wire, bookmark, revision=None):
1012 def bookmark(self, wire, bookmark, revision=None):
1002 repo = self._factory.repo(wire)
1013 repo = self._factory.repo(wire)
1003 baseui = self._factory._create_config(wire['config'])
1014 baseui = self._factory._create_config(wire['config'])
1004 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
1015 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
1005
1016
1006 @reraise_safe_exceptions
1017 @reraise_safe_exceptions
1007 def install_hooks(self, wire, force=False):
1018 def install_hooks(self, wire, force=False):
1008 # we don't need any special hooks for Mercurial
1019 # we don't need any special hooks for Mercurial
1009 pass
1020 pass
1010
1021
1011 @reraise_safe_exceptions
1022 @reraise_safe_exceptions
1012 def get_hooks_info(self, wire):
1023 def get_hooks_info(self, wire):
1013 return {
1024 return {
1014 'pre_version': vcsserver.__version__,
1025 'pre_version': vcsserver.__version__,
1015 'post_version': vcsserver.__version__,
1026 'post_version': vcsserver.__version__,
1016 }
1027 }
1017
1028
1018 @reraise_safe_exceptions
1029 @reraise_safe_exceptions
1019 def set_head_ref(self, wire, head_name):
1030 def set_head_ref(self, wire, head_name):
1020 pass
1031 pass
1021
1032
1022 @reraise_safe_exceptions
1033 @reraise_safe_exceptions
1023 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1034 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1024 archive_dir_name, commit_id):
1035 archive_dir_name, commit_id):
1025
1036
1026 def file_walker(_commit_id, path):
1037 def file_walker(_commit_id, path):
1027 repo = self._factory.repo(wire)
1038 repo = self._factory.repo(wire)
1028 ctx = repo[_commit_id]
1039 ctx = repo[_commit_id]
1029 is_root = path in ['', '/']
1040 is_root = path in ['', '/']
1030 if is_root:
1041 if is_root:
1031 matcher = alwaysmatcher(badfn=None)
1042 matcher = alwaysmatcher(badfn=None)
1032 else:
1043 else:
1033 matcher = patternmatcher('', [(b'glob', path+'/**', b'')], badfn=None)
1044 matcher = patternmatcher('', [(b'glob', path+'/**', b'')], badfn=None)
1034 file_iter = ctx.manifest().walk(matcher)
1045 file_iter = ctx.manifest().walk(matcher)
1035
1046
1036 for fn in file_iter:
1047 for fn in file_iter:
1037 file_path = fn
1048 file_path = fn
1038 flags = ctx.flags(fn)
1049 flags = ctx.flags(fn)
1039 mode = b'x' in flags and 0o755 or 0o644
1050 mode = b'x' in flags and 0o755 or 0o644
1040 is_link = b'l' in flags
1051 is_link = b'l' in flags
1041
1052
1042 yield ArchiveNode(file_path, mode, is_link, ctx[fn].data)
1053 yield ArchiveNode(file_path, mode, is_link, ctx[fn].data_queue)
1043
1054
1044 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1055 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1045 archive_dir_name, commit_id)
1056 archive_dir_name, commit_id)
1046
1057
@@ -1,863 +1,864 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18
18
19 import os
19 import os
20 import subprocess
20 import subprocess
21 from urllib.error import URLError
21 from urllib.error import URLError
22 import urllib.parse
22 import urllib.parse
23 import logging
23 import logging
24 import posixpath as vcspath
24 import posixpath as vcspath
25 import io
25 import io
26 import urllib.request
26 import urllib.request
27 import urllib.parse
27 import urllib.parse
28 import urllib.error
28 import urllib.error
29 import traceback
29 import traceback
30
30
31 import svn.client
31 import svn.client
32 import svn.core
32 import svn.core
33 import svn.delta
33 import svn.delta
34 import svn.diff
34 import svn.diff
35 import svn.fs
35 import svn.fs
36 import svn.repos
36 import svn.repos
37
37
38 from vcsserver import svn_diff, exceptions, subprocessio, settings
38 from vcsserver import svn_diff, exceptions, subprocessio, settings
39 from vcsserver.base import RepoFactory, raise_from_original, ArchiveNode, archive_repo
39 from vcsserver.base import RepoFactory, raise_from_original, ArchiveNode, archive_repo
40 from vcsserver.exceptions import NoContentException
40 from vcsserver.exceptions import NoContentException
41 from vcsserver.utils import safe_str
41 from vcsserver.utils import safe_str
42 from vcsserver.vcs_base import RemoteBase
42 from vcsserver.vcs_base import RemoteBase
43 from vcsserver.lib.svnremoterepo import svnremoterepo
43 from vcsserver.lib.svnremoterepo import svnremoterepo
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 svn_compatible_versions_map = {
47 svn_compatible_versions_map = {
48 'pre-1.4-compatible': '1.3',
48 'pre-1.4-compatible': '1.3',
49 'pre-1.5-compatible': '1.4',
49 'pre-1.5-compatible': '1.4',
50 'pre-1.6-compatible': '1.5',
50 'pre-1.6-compatible': '1.5',
51 'pre-1.8-compatible': '1.7',
51 'pre-1.8-compatible': '1.7',
52 'pre-1.9-compatible': '1.8',
52 'pre-1.9-compatible': '1.8',
53 }
53 }
54
54
55 current_compatible_version = '1.14'
55 current_compatible_version = '1.14'
56
56
57
57
58 def reraise_safe_exceptions(func):
58 def reraise_safe_exceptions(func):
59 """Decorator for converting svn exceptions to something neutral."""
59 """Decorator for converting svn exceptions to something neutral."""
60 def wrapper(*args, **kwargs):
60 def wrapper(*args, **kwargs):
61 try:
61 try:
62 return func(*args, **kwargs)
62 return func(*args, **kwargs)
63 except Exception as e:
63 except Exception as e:
64 if not hasattr(e, '_vcs_kind'):
64 if not hasattr(e, '_vcs_kind'):
65 log.exception("Unhandled exception in svn remote call")
65 log.exception("Unhandled exception in svn remote call")
66 raise_from_original(exceptions.UnhandledException(e))
66 raise_from_original(exceptions.UnhandledException(e))
67 raise
67 raise
68 return wrapper
68 return wrapper
69
69
70
70
71 class SubversionFactory(RepoFactory):
71 class SubversionFactory(RepoFactory):
72 repo_type = 'svn'
72 repo_type = 'svn'
73
73
74 def _create_repo(self, wire, create, compatible_version):
74 def _create_repo(self, wire, create, compatible_version):
75 path = svn.core.svn_path_canonicalize(wire['path'])
75 path = svn.core.svn_path_canonicalize(wire['path'])
76 if create:
76 if create:
77 fs_config = {'compatible-version': current_compatible_version}
77 fs_config = {'compatible-version': current_compatible_version}
78 if compatible_version:
78 if compatible_version:
79
79
80 compatible_version_string = \
80 compatible_version_string = \
81 svn_compatible_versions_map.get(compatible_version) \
81 svn_compatible_versions_map.get(compatible_version) \
82 or compatible_version
82 or compatible_version
83 fs_config['compatible-version'] = compatible_version_string
83 fs_config['compatible-version'] = compatible_version_string
84
84
85 log.debug('Create SVN repo with config "%s"', fs_config)
85 log.debug('Create SVN repo with config "%s"', fs_config)
86 repo = svn.repos.create(path, "", "", None, fs_config)
86 repo = svn.repos.create(path, "", "", None, fs_config)
87 else:
87 else:
88 repo = svn.repos.open(path)
88 repo = svn.repos.open(path)
89
89
90 log.debug('Got SVN object: %s', repo)
90 log.debug('Got SVN object: %s', repo)
91 return repo
91 return repo
92
92
93 def repo(self, wire, create=False, compatible_version=None):
93 def repo(self, wire, create=False, compatible_version=None):
94 """
94 """
95 Get a repository instance for the given path.
95 Get a repository instance for the given path.
96 """
96 """
97 return self._create_repo(wire, create, compatible_version)
97 return self._create_repo(wire, create, compatible_version)
98
98
99
99
100 NODE_TYPE_MAPPING = {
100 NODE_TYPE_MAPPING = {
101 svn.core.svn_node_file: 'file',
101 svn.core.svn_node_file: 'file',
102 svn.core.svn_node_dir: 'dir',
102 svn.core.svn_node_dir: 'dir',
103 }
103 }
104
104
105
105
106 class SvnRemote(RemoteBase):
106 class SvnRemote(RemoteBase):
107
107
108 def __init__(self, factory, hg_factory=None):
108 def __init__(self, factory, hg_factory=None):
109 self._factory = factory
109 self._factory = factory
110
110
111 @reraise_safe_exceptions
111 @reraise_safe_exceptions
112 def discover_svn_version(self):
112 def discover_svn_version(self):
113 try:
113 try:
114 import svn.core
114 import svn.core
115 svn_ver = svn.core.SVN_VERSION
115 svn_ver = svn.core.SVN_VERSION
116 except ImportError:
116 except ImportError:
117 svn_ver = None
117 svn_ver = None
118 return svn_ver
118 return svn_ver
119
119
120 @reraise_safe_exceptions
120 @reraise_safe_exceptions
121 def is_empty(self, wire):
121 def is_empty(self, wire):
122
122
123 try:
123 try:
124 return self.lookup(wire, -1) == 0
124 return self.lookup(wire, -1) == 0
125 except Exception:
125 except Exception:
126 log.exception("failed to read object_store")
126 log.exception("failed to read object_store")
127 return False
127 return False
128
128
129 def check_url(self, url):
129 def check_url(self, url):
130
130
131 # uuid function get's only valid UUID from proper repo, else
131 # uuid function get's only valid UUID from proper repo, else
132 # throws exception
132 # throws exception
133 username, password, src_url = self.get_url_and_credentials(url)
133 username, password, src_url = self.get_url_and_credentials(url)
134 try:
134 try:
135 svnremoterepo(username, password, src_url).svn().uuid
135 svnremoterepo(username, password, src_url).svn().uuid
136 except Exception:
136 except Exception:
137 tb = traceback.format_exc()
137 tb = traceback.format_exc()
138 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
138 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
139 raise URLError(
139 raise URLError(
140 '"%s" is not a valid Subversion source url.' % (url, ))
140 '"%s" is not a valid Subversion source url.' % (url, ))
141 return True
141 return True
142
142
143 def is_path_valid_repository(self, wire, path):
143 def is_path_valid_repository(self, wire, path):
144
144
145 # NOTE(marcink): short circuit the check for SVN repo
145 # NOTE(marcink): short circuit the check for SVN repo
146 # the repos.open might be expensive to check, but we have one cheap
146 # the repos.open might be expensive to check, but we have one cheap
147 # pre condition that we can use, to check for 'format' file
147 # pre condition that we can use, to check for 'format' file
148
148
149 if not os.path.isfile(os.path.join(path, 'format')):
149 if not os.path.isfile(os.path.join(path, 'format')):
150 return False
150 return False
151
151
152 try:
152 try:
153 svn.repos.open(path)
153 svn.repos.open(path)
154 except svn.core.SubversionException:
154 except svn.core.SubversionException:
155 tb = traceback.format_exc()
155 tb = traceback.format_exc()
156 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
156 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
157 return False
157 return False
158 return True
158 return True
159
159
160 @reraise_safe_exceptions
160 @reraise_safe_exceptions
161 def verify(self, wire,):
161 def verify(self, wire,):
162 repo_path = wire['path']
162 repo_path = wire['path']
163 if not self.is_path_valid_repository(wire, repo_path):
163 if not self.is_path_valid_repository(wire, repo_path):
164 raise Exception(
164 raise Exception(
165 "Path %s is not a valid Subversion repository." % repo_path)
165 "Path %s is not a valid Subversion repository." % repo_path)
166
166
167 cmd = ['svnadmin', 'info', repo_path]
167 cmd = ['svnadmin', 'info', repo_path]
168 stdout, stderr = subprocessio.run_command(cmd)
168 stdout, stderr = subprocessio.run_command(cmd)
169 return stdout
169 return stdout
170
170
171 def lookup(self, wire, revision):
171 def lookup(self, wire, revision):
172 if revision not in [-1, None, 'HEAD']:
172 if revision not in [-1, None, 'HEAD']:
173 raise NotImplementedError
173 raise NotImplementedError
174 repo = self._factory.repo(wire)
174 repo = self._factory.repo(wire)
175 fs_ptr = svn.repos.fs(repo)
175 fs_ptr = svn.repos.fs(repo)
176 head = svn.fs.youngest_rev(fs_ptr)
176 head = svn.fs.youngest_rev(fs_ptr)
177 return head
177 return head
178
178
179 def lookup_interval(self, wire, start_ts, end_ts):
179 def lookup_interval(self, wire, start_ts, end_ts):
180 repo = self._factory.repo(wire)
180 repo = self._factory.repo(wire)
181 fsobj = svn.repos.fs(repo)
181 fsobj = svn.repos.fs(repo)
182 start_rev = None
182 start_rev = None
183 end_rev = None
183 end_rev = None
184 if start_ts:
184 if start_ts:
185 start_ts_svn = apr_time_t(start_ts)
185 start_ts_svn = apr_time_t(start_ts)
186 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
186 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
187 else:
187 else:
188 start_rev = 1
188 start_rev = 1
189 if end_ts:
189 if end_ts:
190 end_ts_svn = apr_time_t(end_ts)
190 end_ts_svn = apr_time_t(end_ts)
191 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
191 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
192 else:
192 else:
193 end_rev = svn.fs.youngest_rev(fsobj)
193 end_rev = svn.fs.youngest_rev(fsobj)
194 return start_rev, end_rev
194 return start_rev, end_rev
195
195
196 def revision_properties(self, wire, revision):
196 def revision_properties(self, wire, revision):
197
197
198 cache_on, context_uid, repo_id = self._cache_on(wire)
198 cache_on, context_uid, repo_id = self._cache_on(wire)
199 region = self._region(wire)
199 region = self._region(wire)
200 @region.conditional_cache_on_arguments(condition=cache_on)
200 @region.conditional_cache_on_arguments(condition=cache_on)
201 def _revision_properties(_repo_id, _revision):
201 def _revision_properties(_repo_id, _revision):
202 repo = self._factory.repo(wire)
202 repo = self._factory.repo(wire)
203 fs_ptr = svn.repos.fs(repo)
203 fs_ptr = svn.repos.fs(repo)
204 return svn.fs.revision_proplist(fs_ptr, revision)
204 return svn.fs.revision_proplist(fs_ptr, revision)
205 return _revision_properties(repo_id, revision)
205 return _revision_properties(repo_id, revision)
206
206
207 def revision_changes(self, wire, revision):
207 def revision_changes(self, wire, revision):
208
208
209 repo = self._factory.repo(wire)
209 repo = self._factory.repo(wire)
210 fsobj = svn.repos.fs(repo)
210 fsobj = svn.repos.fs(repo)
211 rev_root = svn.fs.revision_root(fsobj, revision)
211 rev_root = svn.fs.revision_root(fsobj, revision)
212
212
213 editor = svn.repos.ChangeCollector(fsobj, rev_root)
213 editor = svn.repos.ChangeCollector(fsobj, rev_root)
214 editor_ptr, editor_baton = svn.delta.make_editor(editor)
214 editor_ptr, editor_baton = svn.delta.make_editor(editor)
215 base_dir = ""
215 base_dir = ""
216 send_deltas = False
216 send_deltas = False
217 svn.repos.replay2(
217 svn.repos.replay2(
218 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
218 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
219 editor_ptr, editor_baton, None)
219 editor_ptr, editor_baton, None)
220
220
221 added = []
221 added = []
222 changed = []
222 changed = []
223 removed = []
223 removed = []
224
224
225 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
225 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
226 for path, change in editor.changes.items():
226 for path, change in editor.changes.items():
227 # TODO: Decide what to do with directory nodes. Subversion can add
227 # TODO: Decide what to do with directory nodes. Subversion can add
228 # empty directories.
228 # empty directories.
229
229
230 if change.item_kind == svn.core.svn_node_dir:
230 if change.item_kind == svn.core.svn_node_dir:
231 continue
231 continue
232 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
232 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
233 added.append(path)
233 added.append(path)
234 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
234 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
235 svn.repos.CHANGE_ACTION_REPLACE]:
235 svn.repos.CHANGE_ACTION_REPLACE]:
236 changed.append(path)
236 changed.append(path)
237 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
237 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
238 removed.append(path)
238 removed.append(path)
239 else:
239 else:
240 raise NotImplementedError(
240 raise NotImplementedError(
241 "Action %s not supported on path %s" % (
241 "Action %s not supported on path %s" % (
242 change.action, path))
242 change.action, path))
243
243
244 changes = {
244 changes = {
245 'added': added,
245 'added': added,
246 'changed': changed,
246 'changed': changed,
247 'removed': removed,
247 'removed': removed,
248 }
248 }
249 return changes
249 return changes
250
250
251 @reraise_safe_exceptions
251 @reraise_safe_exceptions
252 def node_history(self, wire, path, revision, limit):
252 def node_history(self, wire, path, revision, limit):
253 cache_on, context_uid, repo_id = self._cache_on(wire)
253 cache_on, context_uid, repo_id = self._cache_on(wire)
254 region = self._region(wire)
254 region = self._region(wire)
255 @region.conditional_cache_on_arguments(condition=cache_on)
255 @region.conditional_cache_on_arguments(condition=cache_on)
256 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
256 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
257 cross_copies = False
257 cross_copies = False
258 repo = self._factory.repo(wire)
258 repo = self._factory.repo(wire)
259 fsobj = svn.repos.fs(repo)
259 fsobj = svn.repos.fs(repo)
260 rev_root = svn.fs.revision_root(fsobj, revision)
260 rev_root = svn.fs.revision_root(fsobj, revision)
261
261
262 history_revisions = []
262 history_revisions = []
263 history = svn.fs.node_history(rev_root, path)
263 history = svn.fs.node_history(rev_root, path)
264 history = svn.fs.history_prev(history, cross_copies)
264 history = svn.fs.history_prev(history, cross_copies)
265 while history:
265 while history:
266 __, node_revision = svn.fs.history_location(history)
266 __, node_revision = svn.fs.history_location(history)
267 history_revisions.append(node_revision)
267 history_revisions.append(node_revision)
268 if limit and len(history_revisions) >= limit:
268 if limit and len(history_revisions) >= limit:
269 break
269 break
270 history = svn.fs.history_prev(history, cross_copies)
270 history = svn.fs.history_prev(history, cross_copies)
271 return history_revisions
271 return history_revisions
272 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
272 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
273
273
274 def node_properties(self, wire, path, revision):
274 def node_properties(self, wire, path, revision):
275 cache_on, context_uid, repo_id = self._cache_on(wire)
275 cache_on, context_uid, repo_id = self._cache_on(wire)
276 region = self._region(wire)
276 region = self._region(wire)
277 @region.conditional_cache_on_arguments(condition=cache_on)
277 @region.conditional_cache_on_arguments(condition=cache_on)
278 def _node_properties(_repo_id, _path, _revision):
278 def _node_properties(_repo_id, _path, _revision):
279 repo = self._factory.repo(wire)
279 repo = self._factory.repo(wire)
280 fsobj = svn.repos.fs(repo)
280 fsobj = svn.repos.fs(repo)
281 rev_root = svn.fs.revision_root(fsobj, revision)
281 rev_root = svn.fs.revision_root(fsobj, revision)
282 return svn.fs.node_proplist(rev_root, path)
282 return svn.fs.node_proplist(rev_root, path)
283 return _node_properties(repo_id, path, revision)
283 return _node_properties(repo_id, path, revision)
284
284
285 def file_annotate(self, wire, path, revision):
285 def file_annotate(self, wire, path, revision):
286 abs_path = 'file://' + urllib.pathname2url(
286 abs_path = 'file://' + urllib.pathname2url(
287 vcspath.join(wire['path'], path))
287 vcspath.join(wire['path'], path))
288 file_uri = svn.core.svn_path_canonicalize(abs_path)
288 file_uri = svn.core.svn_path_canonicalize(abs_path)
289
289
290 start_rev = svn_opt_revision_value_t(0)
290 start_rev = svn_opt_revision_value_t(0)
291 peg_rev = svn_opt_revision_value_t(revision)
291 peg_rev = svn_opt_revision_value_t(revision)
292 end_rev = peg_rev
292 end_rev = peg_rev
293
293
294 annotations = []
294 annotations = []
295
295
296 def receiver(line_no, revision, author, date, line, pool):
296 def receiver(line_no, revision, author, date, line, pool):
297 annotations.append((line_no, revision, line))
297 annotations.append((line_no, revision, line))
298
298
299 # TODO: Cannot use blame5, missing typemap function in the swig code
299 # TODO: Cannot use blame5, missing typemap function in the swig code
300 try:
300 try:
301 svn.client.blame2(
301 svn.client.blame2(
302 file_uri, peg_rev, start_rev, end_rev,
302 file_uri, peg_rev, start_rev, end_rev,
303 receiver, svn.client.create_context())
303 receiver, svn.client.create_context())
304 except svn.core.SubversionException as exc:
304 except svn.core.SubversionException as exc:
305 log.exception("Error during blame operation.")
305 log.exception("Error during blame operation.")
306 raise Exception(
306 raise Exception(
307 "Blame not supported or file does not exist at path %s. "
307 "Blame not supported or file does not exist at path %s. "
308 "Error %s." % (path, exc))
308 "Error %s." % (path, exc))
309
309
310 return annotations
310 return annotations
311
311
312 def get_node_type(self, wire, path, revision=None):
312 def get_node_type(self, wire, path, revision=None):
313
313
314 cache_on, context_uid, repo_id = self._cache_on(wire)
314 cache_on, context_uid, repo_id = self._cache_on(wire)
315 region = self._region(wire)
315 region = self._region(wire)
316 @region.conditional_cache_on_arguments(condition=cache_on)
316 @region.conditional_cache_on_arguments(condition=cache_on)
317 def _get_node_type(_repo_id, _path, _revision):
317 def _get_node_type(_repo_id, _path, _revision):
318 repo = self._factory.repo(wire)
318 repo = self._factory.repo(wire)
319 fs_ptr = svn.repos.fs(repo)
319 fs_ptr = svn.repos.fs(repo)
320 if _revision is None:
320 if _revision is None:
321 _revision = svn.fs.youngest_rev(fs_ptr)
321 _revision = svn.fs.youngest_rev(fs_ptr)
322 root = svn.fs.revision_root(fs_ptr, _revision)
322 root = svn.fs.revision_root(fs_ptr, _revision)
323 node = svn.fs.check_path(root, path)
323 node = svn.fs.check_path(root, path)
324 return NODE_TYPE_MAPPING.get(node, None)
324 return NODE_TYPE_MAPPING.get(node, None)
325 return _get_node_type(repo_id, path, revision)
325 return _get_node_type(repo_id, path, revision)
326
326
327 def get_nodes(self, wire, path, revision=None):
327 def get_nodes(self, wire, path, revision=None):
328
328
329 cache_on, context_uid, repo_id = self._cache_on(wire)
329 cache_on, context_uid, repo_id = self._cache_on(wire)
330 region = self._region(wire)
330 region = self._region(wire)
331 @region.conditional_cache_on_arguments(condition=cache_on)
331 @region.conditional_cache_on_arguments(condition=cache_on)
332 def _get_nodes(_repo_id, _path, _revision):
332 def _get_nodes(_repo_id, _path, _revision):
333 repo = self._factory.repo(wire)
333 repo = self._factory.repo(wire)
334 fsobj = svn.repos.fs(repo)
334 fsobj = svn.repos.fs(repo)
335 if _revision is None:
335 if _revision is None:
336 _revision = svn.fs.youngest_rev(fsobj)
336 _revision = svn.fs.youngest_rev(fsobj)
337 root = svn.fs.revision_root(fsobj, _revision)
337 root = svn.fs.revision_root(fsobj, _revision)
338 entries = svn.fs.dir_entries(root, path)
338 entries = svn.fs.dir_entries(root, path)
339 result = []
339 result = []
340 for entry_path, entry_info in entries.items():
340 for entry_path, entry_info in entries.items():
341 result.append(
341 result.append(
342 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
342 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
343 return result
343 return result
344 return _get_nodes(repo_id, path, revision)
344 return _get_nodes(repo_id, path, revision)
345
345
346 def get_file_content(self, wire, path, rev=None):
346 def get_file_content(self, wire, path, rev=None):
347 repo = self._factory.repo(wire)
347 repo = self._factory.repo(wire)
348 fsobj = svn.repos.fs(repo)
348 fsobj = svn.repos.fs(repo)
349 if rev is None:
349 if rev is None:
350 rev = svn.fs.youngest_revision(fsobj)
350 rev = svn.fs.youngest_revision(fsobj)
351 root = svn.fs.revision_root(fsobj, rev)
351 root = svn.fs.revision_root(fsobj, rev)
352 content = svn.core.Stream(svn.fs.file_contents(root, path))
352 content = svn.core.Stream(svn.fs.file_contents(root, path))
353 return content.read()
353 return content.read()
354
354
355 def get_file_size(self, wire, path, revision=None):
355 def get_file_size(self, wire, path, revision=None):
356
356
357 cache_on, context_uid, repo_id = self._cache_on(wire)
357 cache_on, context_uid, repo_id = self._cache_on(wire)
358 region = self._region(wire)
358 region = self._region(wire)
359
359
360 @region.conditional_cache_on_arguments(condition=cache_on)
360 @region.conditional_cache_on_arguments(condition=cache_on)
361 def _get_file_size(_repo_id, _path, _revision):
361 def _get_file_size(_repo_id, _path, _revision):
362 repo = self._factory.repo(wire)
362 repo = self._factory.repo(wire)
363 fsobj = svn.repos.fs(repo)
363 fsobj = svn.repos.fs(repo)
364 if _revision is None:
364 if _revision is None:
365 _revision = svn.fs.youngest_revision(fsobj)
365 _revision = svn.fs.youngest_revision(fsobj)
366 root = svn.fs.revision_root(fsobj, _revision)
366 root = svn.fs.revision_root(fsobj, _revision)
367 size = svn.fs.file_length(root, path)
367 size = svn.fs.file_length(root, path)
368 return size
368 return size
369 return _get_file_size(repo_id, path, revision)
369 return _get_file_size(repo_id, path, revision)
370
370
371 def create_repository(self, wire, compatible_version=None):
371 def create_repository(self, wire, compatible_version=None):
372 log.info('Creating Subversion repository in path "%s"', wire['path'])
372 log.info('Creating Subversion repository in path "%s"', wire['path'])
373 self._factory.repo(wire, create=True,
373 self._factory.repo(wire, create=True,
374 compatible_version=compatible_version)
374 compatible_version=compatible_version)
375
375
376 def get_url_and_credentials(self, src_url):
376 def get_url_and_credentials(self, src_url):
377 obj = urllib.parse.urlparse(src_url)
377 obj = urllib.parse.urlparse(src_url)
378 username = obj.username or None
378 username = obj.username or None
379 password = obj.password or None
379 password = obj.password or None
380 return username, password, src_url
380 return username, password, src_url
381
381
382 def import_remote_repository(self, wire, src_url):
382 def import_remote_repository(self, wire, src_url):
383 repo_path = wire['path']
383 repo_path = wire['path']
384 if not self.is_path_valid_repository(wire, repo_path):
384 if not self.is_path_valid_repository(wire, repo_path):
385 raise Exception(
385 raise Exception(
386 "Path %s is not a valid Subversion repository." % repo_path)
386 "Path %s is not a valid Subversion repository." % repo_path)
387
387
388 username, password, src_url = self.get_url_and_credentials(src_url)
388 username, password, src_url = self.get_url_and_credentials(src_url)
389 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
389 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
390 '--trust-server-cert-failures=unknown-ca']
390 '--trust-server-cert-failures=unknown-ca']
391 if username and password:
391 if username and password:
392 rdump_cmd += ['--username', username, '--password', password]
392 rdump_cmd += ['--username', username, '--password', password]
393 rdump_cmd += [src_url]
393 rdump_cmd += [src_url]
394
394
395 rdump = subprocess.Popen(
395 rdump = subprocess.Popen(
396 rdump_cmd,
396 rdump_cmd,
397 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
397 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
398 load = subprocess.Popen(
398 load = subprocess.Popen(
399 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
399 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
400
400
401 # TODO: johbo: This can be a very long operation, might be better
401 # TODO: johbo: This can be a very long operation, might be better
402 # to track some kind of status and provide an api to check if the
402 # to track some kind of status and provide an api to check if the
403 # import is done.
403 # import is done.
404 rdump.wait()
404 rdump.wait()
405 load.wait()
405 load.wait()
406
406
407 log.debug('Return process ended with code: %s', rdump.returncode)
407 log.debug('Return process ended with code: %s', rdump.returncode)
408 if rdump.returncode != 0:
408 if rdump.returncode != 0:
409 errors = rdump.stderr.read()
409 errors = rdump.stderr.read()
410 log.error('svnrdump dump failed: statuscode %s: message: %s',
410 log.error('svnrdump dump failed: statuscode %s: message: %s', rdump.returncode, errors)
411 rdump.returncode, errors)
411
412 reason = 'UNKNOWN'
412 reason = 'UNKNOWN'
413 if 'svnrdump: E230001:' in errors:
413 if b'svnrdump: E230001:' in errors:
414 reason = 'INVALID_CERTIFICATE'
414 reason = 'INVALID_CERTIFICATE'
415
415
416 if reason == 'UNKNOWN':
416 if reason == 'UNKNOWN':
417 reason = 'UNKNOWN:{}'.format(errors)
417 reason = 'UNKNOWN:{}'.format(safe_str(errors))
418
418 raise Exception(
419 raise Exception(
419 'Failed to dump the remote repository from %s. Reason:%s' % (
420 'Failed to dump the remote repository from %s. Reason:%s' % (
420 src_url, reason))
421 src_url, reason))
421 if load.returncode != 0:
422 if load.returncode != 0:
422 raise Exception(
423 raise Exception(
423 'Failed to load the dump of remote repository from %s.' %
424 'Failed to load the dump of remote repository from %s.' %
424 (src_url, ))
425 (src_url, ))
425
426
426 def commit(self, wire, message, author, timestamp, updated, removed):
427 def commit(self, wire, message, author, timestamp, updated, removed):
427 assert isinstance(message, str)
428 assert isinstance(message, str)
428 assert isinstance(author, str)
429 assert isinstance(author, str)
429
430
430 repo = self._factory.repo(wire)
431 repo = self._factory.repo(wire)
431 fsobj = svn.repos.fs(repo)
432 fsobj = svn.repos.fs(repo)
432
433
433 rev = svn.fs.youngest_rev(fsobj)
434 rev = svn.fs.youngest_rev(fsobj)
434 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
435 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
435 txn_root = svn.fs.txn_root(txn)
436 txn_root = svn.fs.txn_root(txn)
436
437
437 for node in updated:
438 for node in updated:
438 TxnNodeProcessor(node, txn_root).update()
439 TxnNodeProcessor(node, txn_root).update()
439 for node in removed:
440 for node in removed:
440 TxnNodeProcessor(node, txn_root).remove()
441 TxnNodeProcessor(node, txn_root).remove()
441
442
442 commit_id = svn.repos.fs_commit_txn(repo, txn)
443 commit_id = svn.repos.fs_commit_txn(repo, txn)
443
444
444 if timestamp:
445 if timestamp:
445 apr_time = apr_time_t(timestamp)
446 apr_time = apr_time_t(timestamp)
446 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
447 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
447 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
448 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
448
449
449 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
450 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
450 return commit_id
451 return commit_id
451
452
452 def diff(self, wire, rev1, rev2, path1=None, path2=None,
453 def diff(self, wire, rev1, rev2, path1=None, path2=None,
453 ignore_whitespace=False, context=3):
454 ignore_whitespace=False, context=3):
454
455
455 wire.update(cache=False)
456 wire.update(cache=False)
456 repo = self._factory.repo(wire)
457 repo = self._factory.repo(wire)
457 diff_creator = SvnDiffer(
458 diff_creator = SvnDiffer(
458 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
459 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
459 try:
460 try:
460 return diff_creator.generate_diff()
461 return diff_creator.generate_diff()
461 except svn.core.SubversionException as e:
462 except svn.core.SubversionException as e:
462 log.exception(
463 log.exception(
463 "Error during diff operation operation. "
464 "Error during diff operation operation. "
464 "Path might not exist %s, %s" % (path1, path2))
465 "Path might not exist %s, %s" % (path1, path2))
465 return ""
466 return ""
466
467
467 @reraise_safe_exceptions
468 @reraise_safe_exceptions
468 def is_large_file(self, wire, path):
469 def is_large_file(self, wire, path):
469 return False
470 return False
470
471
471 @reraise_safe_exceptions
472 @reraise_safe_exceptions
472 def is_binary(self, wire, rev, path):
473 def is_binary(self, wire, rev, path):
473 cache_on, context_uid, repo_id = self._cache_on(wire)
474 cache_on, context_uid, repo_id = self._cache_on(wire)
474
475
475 region = self._region(wire)
476 region = self._region(wire)
476 @region.conditional_cache_on_arguments(condition=cache_on)
477 @region.conditional_cache_on_arguments(condition=cache_on)
477 def _is_binary(_repo_id, _rev, _path):
478 def _is_binary(_repo_id, _rev, _path):
478 raw_bytes = self.get_file_content(wire, path, rev)
479 raw_bytes = self.get_file_content(wire, path, rev)
479 return raw_bytes and '\0' in raw_bytes
480 return raw_bytes and '\0' in raw_bytes
480
481
481 return _is_binary(repo_id, rev, path)
482 return _is_binary(repo_id, rev, path)
482
483
483 @reraise_safe_exceptions
484 @reraise_safe_exceptions
484 def run_svn_command(self, wire, cmd, **opts):
485 def run_svn_command(self, wire, cmd, **opts):
485 path = wire.get('path', None)
486 path = wire.get('path', None)
486
487
487 if path and os.path.isdir(path):
488 if path and os.path.isdir(path):
488 opts['cwd'] = path
489 opts['cwd'] = path
489
490
490 safe_call = opts.pop('_safe', False)
491 safe_call = opts.pop('_safe', False)
491
492
492 svnenv = os.environ.copy()
493 svnenv = os.environ.copy()
493 svnenv.update(opts.pop('extra_env', {}))
494 svnenv.update(opts.pop('extra_env', {}))
494
495
495 _opts = {'env': svnenv, 'shell': False}
496 _opts = {'env': svnenv, 'shell': False}
496
497
497 try:
498 try:
498 _opts.update(opts)
499 _opts.update(opts)
499 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
500 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
500
501
501 return ''.join(p), ''.join(p.error)
502 return b''.join(proc), b''.join(proc.stderr)
502 except (EnvironmentError, OSError) as err:
503 except OSError as err:
503 if safe_call:
504 if safe_call:
504 return '', safe_str(err).strip()
505 return '', safe_str(err).strip()
505 else:
506 else:
506 cmd = ' '.join(cmd) # human friendly CMD
507 cmd = ' '.join(cmd) # human friendly CMD
507 tb_err = ("Couldn't run svn command (%s).\n"
508 tb_err = ("Couldn't run svn command (%s).\n"
508 "Original error was:%s\n"
509 "Original error was:%s\n"
509 "Call options:%s\n"
510 "Call options:%s\n"
510 % (cmd, err, _opts))
511 % (cmd, err, _opts))
511 log.exception(tb_err)
512 log.exception(tb_err)
512 raise exceptions.VcsException()(tb_err)
513 raise exceptions.VcsException()(tb_err)
513
514
514 @reraise_safe_exceptions
515 @reraise_safe_exceptions
515 def install_hooks(self, wire, force=False):
516 def install_hooks(self, wire, force=False):
516 from vcsserver.hook_utils import install_svn_hooks
517 from vcsserver.hook_utils import install_svn_hooks
517 repo_path = wire['path']
518 repo_path = wire['path']
518 binary_dir = settings.BINARY_DIR
519 binary_dir = settings.BINARY_DIR
519 executable = None
520 executable = None
520 if binary_dir:
521 if binary_dir:
521 executable = os.path.join(binary_dir, 'python')
522 executable = os.path.join(binary_dir, 'python')
522 return install_svn_hooks(
523 return install_svn_hooks(
523 repo_path, executable=executable, force_create=force)
524 repo_path, executable=executable, force_create=force)
524
525
525 @reraise_safe_exceptions
526 @reraise_safe_exceptions
526 def get_hooks_info(self, wire):
527 def get_hooks_info(self, wire):
527 from vcsserver.hook_utils import (
528 from vcsserver.hook_utils import (
528 get_svn_pre_hook_version, get_svn_post_hook_version)
529 get_svn_pre_hook_version, get_svn_post_hook_version)
529 repo_path = wire['path']
530 repo_path = wire['path']
530 return {
531 return {
531 'pre_version': get_svn_pre_hook_version(repo_path),
532 'pre_version': get_svn_pre_hook_version(repo_path),
532 'post_version': get_svn_post_hook_version(repo_path),
533 'post_version': get_svn_post_hook_version(repo_path),
533 }
534 }
534
535
535 @reraise_safe_exceptions
536 @reraise_safe_exceptions
536 def set_head_ref(self, wire, head_name):
537 def set_head_ref(self, wire, head_name):
537 pass
538 pass
538
539
539 @reraise_safe_exceptions
540 @reraise_safe_exceptions
540 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
541 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
541 archive_dir_name, commit_id):
542 archive_dir_name, commit_id):
542
543
543 def walk_tree(root, root_dir, _commit_id):
544 def walk_tree(root, root_dir, _commit_id):
544 """
545 """
545 Special recursive svn repo walker
546 Special recursive svn repo walker
546 """
547 """
547
548
548 filemode_default = 0o100644
549 filemode_default = 0o100644
549 filemode_executable = 0o100755
550 filemode_executable = 0o100755
550
551
551 file_iter = svn.fs.dir_entries(root, root_dir)
552 file_iter = svn.fs.dir_entries(root, root_dir)
552 for f_name in file_iter:
553 for f_name in file_iter:
553 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
554 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
554
555
555 if f_type == 'dir':
556 if f_type == 'dir':
556 # return only DIR, and then all entries in that dir
557 # return only DIR, and then all entries in that dir
557 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
558 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
558 new_root = os.path.join(root_dir, f_name)
559 new_root = os.path.join(root_dir, f_name)
559 for _f_name, _f_data, _f_type in walk_tree(root, new_root, _commit_id):
560 for _f_name, _f_data, _f_type in walk_tree(root, new_root, _commit_id):
560 yield _f_name, _f_data, _f_type
561 yield _f_name, _f_data, _f_type
561 else:
562 else:
562 f_path = os.path.join(root_dir, f_name).rstrip('/')
563 f_path = os.path.join(root_dir, f_name).rstrip('/')
563 prop_list = svn.fs.node_proplist(root, f_path)
564 prop_list = svn.fs.node_proplist(root, f_path)
564
565
565 f_mode = filemode_default
566 f_mode = filemode_default
566 if prop_list.get('svn:executable'):
567 if prop_list.get('svn:executable'):
567 f_mode = filemode_executable
568 f_mode = filemode_executable
568
569
569 f_is_link = False
570 f_is_link = False
570 if prop_list.get('svn:special'):
571 if prop_list.get('svn:special'):
571 f_is_link = True
572 f_is_link = True
572
573
573 data = {
574 data = {
574 'is_link': f_is_link,
575 'is_link': f_is_link,
575 'mode': f_mode,
576 'mode': f_mode,
576 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
577 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
577 }
578 }
578
579
579 yield f_path, data, f_type
580 yield f_path, data, f_type
580
581
581 def file_walker(_commit_id, path):
582 def file_walker(_commit_id, path):
582 repo = self._factory.repo(wire)
583 repo = self._factory.repo(wire)
583 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
584 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
584
585
585 def no_content():
586 def no_content():
586 raise NoContentException()
587 raise NoContentException()
587
588
588 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
589 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
589 file_path = f_name
590 file_path = f_name
590
591
591 if f_type == 'dir':
592 if f_type == 'dir':
592 mode = f_data['mode']
593 mode = f_data['mode']
593 yield ArchiveNode(file_path, mode, False, no_content)
594 yield ArchiveNode(file_path, mode, False, no_content)
594 else:
595 else:
595 mode = f_data['mode']
596 mode = f_data['mode']
596 is_link = f_data['is_link']
597 is_link = f_data['is_link']
597 data_stream = f_data['content_stream']
598 data_stream = f_data['content_stream']
598 yield ArchiveNode(file_path, mode, is_link, data_stream)
599 yield ArchiveNode(file_path, mode, is_link, data_stream)
599
600
600 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
601 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
601 archive_dir_name, commit_id)
602 archive_dir_name, commit_id)
602
603
603
604
604 class SvnDiffer(object):
605 class SvnDiffer(object):
605 """
606 """
606 Utility to create diffs based on difflib and the Subversion api
607 Utility to create diffs based on difflib and the Subversion api
607 """
608 """
608
609
609 binary_content = False
610 binary_content = False
610
611
611 def __init__(
612 def __init__(
612 self, repo, src_rev, src_path, tgt_rev, tgt_path,
613 self, repo, src_rev, src_path, tgt_rev, tgt_path,
613 ignore_whitespace, context):
614 ignore_whitespace, context):
614 self.repo = repo
615 self.repo = repo
615 self.ignore_whitespace = ignore_whitespace
616 self.ignore_whitespace = ignore_whitespace
616 self.context = context
617 self.context = context
617
618
618 fsobj = svn.repos.fs(repo)
619 fsobj = svn.repos.fs(repo)
619
620
620 self.tgt_rev = tgt_rev
621 self.tgt_rev = tgt_rev
621 self.tgt_path = tgt_path or ''
622 self.tgt_path = tgt_path or ''
622 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
623 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
623 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
624 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
624
625
625 self.src_rev = src_rev
626 self.src_rev = src_rev
626 self.src_path = src_path or self.tgt_path
627 self.src_path = src_path or self.tgt_path
627 self.src_root = svn.fs.revision_root(fsobj, src_rev)
628 self.src_root = svn.fs.revision_root(fsobj, src_rev)
628 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
629 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
629
630
630 self._validate()
631 self._validate()
631
632
632 def _validate(self):
633 def _validate(self):
633 if (self.tgt_kind != svn.core.svn_node_none and
634 if (self.tgt_kind != svn.core.svn_node_none and
634 self.src_kind != svn.core.svn_node_none and
635 self.src_kind != svn.core.svn_node_none and
635 self.src_kind != self.tgt_kind):
636 self.src_kind != self.tgt_kind):
636 # TODO: johbo: proper error handling
637 # TODO: johbo: proper error handling
637 raise Exception(
638 raise Exception(
638 "Source and target are not compatible for diff generation. "
639 "Source and target are not compatible for diff generation. "
639 "Source type: %s, target type: %s" %
640 "Source type: %s, target type: %s" %
640 (self.src_kind, self.tgt_kind))
641 (self.src_kind, self.tgt_kind))
641
642
642 def generate_diff(self):
643 def generate_diff(self):
643 buf = io.StringIO()
644 buf = io.StringIO()
644 if self.tgt_kind == svn.core.svn_node_dir:
645 if self.tgt_kind == svn.core.svn_node_dir:
645 self._generate_dir_diff(buf)
646 self._generate_dir_diff(buf)
646 else:
647 else:
647 self._generate_file_diff(buf)
648 self._generate_file_diff(buf)
648 return buf.getvalue()
649 return buf.getvalue()
649
650
650 def _generate_dir_diff(self, buf):
651 def _generate_dir_diff(self, buf):
651 editor = DiffChangeEditor()
652 editor = DiffChangeEditor()
652 editor_ptr, editor_baton = svn.delta.make_editor(editor)
653 editor_ptr, editor_baton = svn.delta.make_editor(editor)
653 svn.repos.dir_delta2(
654 svn.repos.dir_delta2(
654 self.src_root,
655 self.src_root,
655 self.src_path,
656 self.src_path,
656 '', # src_entry
657 '', # src_entry
657 self.tgt_root,
658 self.tgt_root,
658 self.tgt_path,
659 self.tgt_path,
659 editor_ptr, editor_baton,
660 editor_ptr, editor_baton,
660 authorization_callback_allow_all,
661 authorization_callback_allow_all,
661 False, # text_deltas
662 False, # text_deltas
662 svn.core.svn_depth_infinity, # depth
663 svn.core.svn_depth_infinity, # depth
663 False, # entry_props
664 False, # entry_props
664 False, # ignore_ancestry
665 False, # ignore_ancestry
665 )
666 )
666
667
667 for path, __, change in sorted(editor.changes):
668 for path, __, change in sorted(editor.changes):
668 self._generate_node_diff(
669 self._generate_node_diff(
669 buf, change, path, self.tgt_path, path, self.src_path)
670 buf, change, path, self.tgt_path, path, self.src_path)
670
671
671 def _generate_file_diff(self, buf):
672 def _generate_file_diff(self, buf):
672 change = None
673 change = None
673 if self.src_kind == svn.core.svn_node_none:
674 if self.src_kind == svn.core.svn_node_none:
674 change = "add"
675 change = "add"
675 elif self.tgt_kind == svn.core.svn_node_none:
676 elif self.tgt_kind == svn.core.svn_node_none:
676 change = "delete"
677 change = "delete"
677 tgt_base, tgt_path = vcspath.split(self.tgt_path)
678 tgt_base, tgt_path = vcspath.split(self.tgt_path)
678 src_base, src_path = vcspath.split(self.src_path)
679 src_base, src_path = vcspath.split(self.src_path)
679 self._generate_node_diff(
680 self._generate_node_diff(
680 buf, change, tgt_path, tgt_base, src_path, src_base)
681 buf, change, tgt_path, tgt_base, src_path, src_base)
681
682
682 def _generate_node_diff(
683 def _generate_node_diff(
683 self, buf, change, tgt_path, tgt_base, src_path, src_base):
684 self, buf, change, tgt_path, tgt_base, src_path, src_base):
684
685
685 if self.src_rev == self.tgt_rev and tgt_base == src_base:
686 if self.src_rev == self.tgt_rev and tgt_base == src_base:
686 # makes consistent behaviour with git/hg to return empty diff if
687 # makes consistent behaviour with git/hg to return empty diff if
687 # we compare same revisions
688 # we compare same revisions
688 return
689 return
689
690
690 tgt_full_path = vcspath.join(tgt_base, tgt_path)
691 tgt_full_path = vcspath.join(tgt_base, tgt_path)
691 src_full_path = vcspath.join(src_base, src_path)
692 src_full_path = vcspath.join(src_base, src_path)
692
693
693 self.binary_content = False
694 self.binary_content = False
694 mime_type = self._get_mime_type(tgt_full_path)
695 mime_type = self._get_mime_type(tgt_full_path)
695
696
696 if mime_type and not mime_type.startswith('text'):
697 if mime_type and not mime_type.startswith('text'):
697 self.binary_content = True
698 self.binary_content = True
698 buf.write("=" * 67 + '\n')
699 buf.write("=" * 67 + '\n')
699 buf.write("Cannot display: file marked as a binary type.\n")
700 buf.write("Cannot display: file marked as a binary type.\n")
700 buf.write("svn:mime-type = %s\n" % mime_type)
701 buf.write("svn:mime-type = %s\n" % mime_type)
701 buf.write("Index: %s\n" % (tgt_path, ))
702 buf.write("Index: %s\n" % (tgt_path, ))
702 buf.write("=" * 67 + '\n')
703 buf.write("=" * 67 + '\n')
703 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
704 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
704 'tgt_path': tgt_path})
705 'tgt_path': tgt_path})
705
706
706 if change == 'add':
707 if change == 'add':
707 # TODO: johbo: SVN is missing a zero here compared to git
708 # TODO: johbo: SVN is missing a zero here compared to git
708 buf.write("new file mode 10644\n")
709 buf.write("new file mode 10644\n")
709
710
710 #TODO(marcink): intro to binary detection of svn patches
711 #TODO(marcink): intro to binary detection of svn patches
711 # if self.binary_content:
712 # if self.binary_content:
712 # buf.write('GIT binary patch\n')
713 # buf.write('GIT binary patch\n')
713
714
714 buf.write("--- /dev/null\t(revision 0)\n")
715 buf.write("--- /dev/null\t(revision 0)\n")
715 src_lines = []
716 src_lines = []
716 else:
717 else:
717 if change == 'delete':
718 if change == 'delete':
718 buf.write("deleted file mode 10644\n")
719 buf.write("deleted file mode 10644\n")
719
720
720 #TODO(marcink): intro to binary detection of svn patches
721 #TODO(marcink): intro to binary detection of svn patches
721 # if self.binary_content:
722 # if self.binary_content:
722 # buf.write('GIT binary patch\n')
723 # buf.write('GIT binary patch\n')
723
724
724 buf.write("--- a/%s\t(revision %s)\n" % (
725 buf.write("--- a/%s\t(revision %s)\n" % (
725 src_path, self.src_rev))
726 src_path, self.src_rev))
726 src_lines = self._svn_readlines(self.src_root, src_full_path)
727 src_lines = self._svn_readlines(self.src_root, src_full_path)
727
728
728 if change == 'delete':
729 if change == 'delete':
729 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
730 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
730 tgt_lines = []
731 tgt_lines = []
731 else:
732 else:
732 buf.write("+++ b/%s\t(revision %s)\n" % (
733 buf.write("+++ b/%s\t(revision %s)\n" % (
733 tgt_path, self.tgt_rev))
734 tgt_path, self.tgt_rev))
734 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
735 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
735
736
736 if not self.binary_content:
737 if not self.binary_content:
737 udiff = svn_diff.unified_diff(
738 udiff = svn_diff.unified_diff(
738 src_lines, tgt_lines, context=self.context,
739 src_lines, tgt_lines, context=self.context,
739 ignore_blank_lines=self.ignore_whitespace,
740 ignore_blank_lines=self.ignore_whitespace,
740 ignore_case=False,
741 ignore_case=False,
741 ignore_space_changes=self.ignore_whitespace)
742 ignore_space_changes=self.ignore_whitespace)
742 buf.writelines(udiff)
743 buf.writelines(udiff)
743
744
744 def _get_mime_type(self, path):
745 def _get_mime_type(self, path):
745 try:
746 try:
746 mime_type = svn.fs.node_prop(
747 mime_type = svn.fs.node_prop(
747 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
748 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
748 except svn.core.SubversionException:
749 except svn.core.SubversionException:
749 mime_type = svn.fs.node_prop(
750 mime_type = svn.fs.node_prop(
750 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
751 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
751 return mime_type
752 return mime_type
752
753
753 def _svn_readlines(self, fs_root, node_path):
754 def _svn_readlines(self, fs_root, node_path):
754 if self.binary_content:
755 if self.binary_content:
755 return []
756 return []
756 node_kind = svn.fs.check_path(fs_root, node_path)
757 node_kind = svn.fs.check_path(fs_root, node_path)
757 if node_kind not in (
758 if node_kind not in (
758 svn.core.svn_node_file, svn.core.svn_node_symlink):
759 svn.core.svn_node_file, svn.core.svn_node_symlink):
759 return []
760 return []
760 content = svn.core.Stream(
761 content = svn.core.Stream(
761 svn.fs.file_contents(fs_root, node_path)).read()
762 svn.fs.file_contents(fs_root, node_path)).read()
762 return content.splitlines(True)
763 return content.splitlines(True)
763
764
764
765
765 class DiffChangeEditor(svn.delta.Editor):
766 class DiffChangeEditor(svn.delta.Editor):
766 """
767 """
767 Records changes between two given revisions
768 Records changes between two given revisions
768 """
769 """
769
770
770 def __init__(self):
771 def __init__(self):
771 self.changes = []
772 self.changes = []
772
773
773 def delete_entry(self, path, revision, parent_baton, pool=None):
774 def delete_entry(self, path, revision, parent_baton, pool=None):
774 self.changes.append((path, None, 'delete'))
775 self.changes.append((path, None, 'delete'))
775
776
776 def add_file(
777 def add_file(
777 self, path, parent_baton, copyfrom_path, copyfrom_revision,
778 self, path, parent_baton, copyfrom_path, copyfrom_revision,
778 file_pool=None):
779 file_pool=None):
779 self.changes.append((path, 'file', 'add'))
780 self.changes.append((path, 'file', 'add'))
780
781
781 def open_file(self, path, parent_baton, base_revision, file_pool=None):
782 def open_file(self, path, parent_baton, base_revision, file_pool=None):
782 self.changes.append((path, 'file', 'change'))
783 self.changes.append((path, 'file', 'change'))
783
784
784
785
785 def authorization_callback_allow_all(root, path, pool):
786 def authorization_callback_allow_all(root, path, pool):
786 return True
787 return True
787
788
788
789
789 class TxnNodeProcessor(object):
790 class TxnNodeProcessor(object):
790 """
791 """
791 Utility to process the change of one node within a transaction root.
792 Utility to process the change of one node within a transaction root.
792
793
793 It encapsulates the knowledge of how to add, update or remove
794 It encapsulates the knowledge of how to add, update or remove
794 a node for a given transaction root. The purpose is to support the method
795 a node for a given transaction root. The purpose is to support the method
795 `SvnRemote.commit`.
796 `SvnRemote.commit`.
796 """
797 """
797
798
798 def __init__(self, node, txn_root):
799 def __init__(self, node, txn_root):
799 assert isinstance(node['path'], str)
800 assert isinstance(node['path'], str)
800
801
801 self.node = node
802 self.node = node
802 self.txn_root = txn_root
803 self.txn_root = txn_root
803
804
804 def update(self):
805 def update(self):
805 self._ensure_parent_dirs()
806 self._ensure_parent_dirs()
806 self._add_file_if_node_does_not_exist()
807 self._add_file_if_node_does_not_exist()
807 self._update_file_content()
808 self._update_file_content()
808 self._update_file_properties()
809 self._update_file_properties()
809
810
810 def remove(self):
811 def remove(self):
811 svn.fs.delete(self.txn_root, self.node['path'])
812 svn.fs.delete(self.txn_root, self.node['path'])
812 # TODO: Clean up directory if empty
813 # TODO: Clean up directory if empty
813
814
814 def _ensure_parent_dirs(self):
815 def _ensure_parent_dirs(self):
815 curdir = vcspath.dirname(self.node['path'])
816 curdir = vcspath.dirname(self.node['path'])
816 dirs_to_create = []
817 dirs_to_create = []
817 while not self._svn_path_exists(curdir):
818 while not self._svn_path_exists(curdir):
818 dirs_to_create.append(curdir)
819 dirs_to_create.append(curdir)
819 curdir = vcspath.dirname(curdir)
820 curdir = vcspath.dirname(curdir)
820
821
821 for curdir in reversed(dirs_to_create):
822 for curdir in reversed(dirs_to_create):
822 log.debug('Creating missing directory "%s"', curdir)
823 log.debug('Creating missing directory "%s"', curdir)
823 svn.fs.make_dir(self.txn_root, curdir)
824 svn.fs.make_dir(self.txn_root, curdir)
824
825
825 def _svn_path_exists(self, path):
826 def _svn_path_exists(self, path):
826 path_status = svn.fs.check_path(self.txn_root, path)
827 path_status = svn.fs.check_path(self.txn_root, path)
827 return path_status != svn.core.svn_node_none
828 return path_status != svn.core.svn_node_none
828
829
829 def _add_file_if_node_does_not_exist(self):
830 def _add_file_if_node_does_not_exist(self):
830 kind = svn.fs.check_path(self.txn_root, self.node['path'])
831 kind = svn.fs.check_path(self.txn_root, self.node['path'])
831 if kind == svn.core.svn_node_none:
832 if kind == svn.core.svn_node_none:
832 svn.fs.make_file(self.txn_root, self.node['path'])
833 svn.fs.make_file(self.txn_root, self.node['path'])
833
834
834 def _update_file_content(self):
835 def _update_file_content(self):
835 assert isinstance(self.node['content'], str)
836 assert isinstance(self.node['content'], str)
836 handler, baton = svn.fs.apply_textdelta(
837 handler, baton = svn.fs.apply_textdelta(
837 self.txn_root, self.node['path'], None, None)
838 self.txn_root, self.node['path'], None, None)
838 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
839 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
839
840
840 def _update_file_properties(self):
841 def _update_file_properties(self):
841 properties = self.node.get('properties', {})
842 properties = self.node.get('properties', {})
842 for key, value in properties.items():
843 for key, value in properties.items():
843 svn.fs.change_node_prop(
844 svn.fs.change_node_prop(
844 self.txn_root, self.node['path'], key, value)
845 self.txn_root, self.node['path'], key, value)
845
846
846
847
847 def apr_time_t(timestamp):
848 def apr_time_t(timestamp):
848 """
849 """
849 Convert a Python timestamp into APR timestamp type apr_time_t
850 Convert a Python timestamp into APR timestamp type apr_time_t
850 """
851 """
851 return timestamp * 1E6
852 return timestamp * 1E6
852
853
853
854
854 def svn_opt_revision_value_t(num):
855 def svn_opt_revision_value_t(num):
855 """
856 """
856 Put `num` into a `svn_opt_revision_value_t` structure.
857 Put `num` into a `svn_opt_revision_value_t` structure.
857 """
858 """
858 value = svn.core.svn_opt_revision_value_t()
859 value = svn.core.svn_opt_revision_value_t()
859 value.number = num
860 value.number = num
860 revision = svn.core.svn_opt_revision_t()
861 revision = svn.core.svn_opt_revision_t()
861 revision.kind = svn.core.svn_opt_revision_number
862 revision.kind = svn.core.svn_opt_revision_number
862 revision.value = value
863 revision.value = value
863 return revision
864 return revision
@@ -1,235 +1,235 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import logging
19 import logging
20 import itertools
20 import itertools
21
21
22 import mercurial
22 import mercurial
23 import mercurial.error
23 import mercurial.error
24 import mercurial.wireprotoserver
24 import mercurial.wireprotoserver
25 import mercurial.hgweb.common
25 import mercurial.hgweb.common
26 import mercurial.hgweb.hgweb_mod
26 import mercurial.hgweb.hgweb_mod
27 import webob.exc
27 import webob.exc
28
28
29 from vcsserver import pygrack, exceptions, settings, git_lfs
29 from vcsserver import pygrack, exceptions, settings, git_lfs
30
30 from vcsserver.utils import ascii_bytes
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 # propagated from mercurial documentation
35 # propagated from mercurial documentation
36 HG_UI_SECTIONS = [
36 HG_UI_SECTIONS = [
37 'alias', 'auth', 'decode/encode', 'defaults', 'diff', 'email', 'extensions',
37 'alias', 'auth', 'decode/encode', 'defaults', 'diff', 'email', 'extensions',
38 'format', 'merge-patterns', 'merge-tools', 'hooks', 'http_proxy', 'smtp',
38 'format', 'merge-patterns', 'merge-tools', 'hooks', 'http_proxy', 'smtp',
39 'patch', 'paths', 'profiling', 'server', 'trusted', 'ui', 'web',
39 'patch', 'paths', 'profiling', 'server', 'trusted', 'ui', 'web',
40 ]
40 ]
41
41
42
42
43 class HgWeb(mercurial.hgweb.hgweb_mod.hgweb):
43 class HgWeb(mercurial.hgweb.hgweb_mod.hgweb):
44 """Extension of hgweb that simplifies some functions."""
44 """Extension of hgweb that simplifies some functions."""
45
45
46 def _get_view(self, repo):
46 def _get_view(self, repo):
47 """Views are not supported."""
47 """Views are not supported."""
48 return repo
48 return repo
49
49
50 def loadsubweb(self):
50 def loadsubweb(self):
51 """The result is only used in the templater method which is not used."""
51 """The result is only used in the templater method which is not used."""
52 return None
52 return None
53
53
54 def run(self):
54 def run(self):
55 """Unused function so raise an exception if accidentally called."""
55 """Unused function so raise an exception if accidentally called."""
56 raise NotImplementedError
56 raise NotImplementedError
57
57
58 def templater(self, req):
58 def templater(self, req):
59 """Function used in an unreachable code path.
59 """Function used in an unreachable code path.
60
60
61 This code is unreachable because we guarantee that the HTTP request,
61 This code is unreachable because we guarantee that the HTTP request,
62 corresponds to a Mercurial command. See the is_hg method. So, we are
62 corresponds to a Mercurial command. See the is_hg method. So, we are
63 never going to get a user-visible url.
63 never going to get a user-visible url.
64 """
64 """
65 raise NotImplementedError
65 raise NotImplementedError
66
66
67 def archivelist(self, nodeid):
67 def archivelist(self, nodeid):
68 """Unused function so raise an exception if accidentally called."""
68 """Unused function so raise an exception if accidentally called."""
69 raise NotImplementedError
69 raise NotImplementedError
70
70
71 def __call__(self, environ, start_response):
71 def __call__(self, environ, start_response):
72 """Run the WSGI application.
72 """Run the WSGI application.
73
73
74 This may be called by multiple threads.
74 This may be called by multiple threads.
75 """
75 """
76 from mercurial.hgweb import request as requestmod
76 from mercurial.hgweb import request as requestmod
77 req = requestmod.parserequestfromenv(environ)
77 req = requestmod.parserequestfromenv(environ)
78 res = requestmod.wsgiresponse(req, start_response)
78 res = requestmod.wsgiresponse(req, start_response)
79 gen = self.run_wsgi(req, res)
79 gen = self.run_wsgi(req, res)
80
80
81 first_chunk = None
81 first_chunk = None
82
82
83 try:
83 try:
84 data = next(gen)
84 data = next(gen)
85
85
86 def first_chunk():
86 def first_chunk():
87 yield data
87 yield data
88 except StopIteration:
88 except StopIteration:
89 pass
89 pass
90
90
91 if first_chunk:
91 if first_chunk:
92 return itertools.chain(first_chunk(), gen)
92 return itertools.chain(first_chunk(), gen)
93 return gen
93 return gen
94
94
95 def _runwsgi(self, req, res, repo):
95 def _runwsgi(self, req, res, repo):
96
96
97 cmd = req.qsparams.get('cmd', '')
97 cmd = req.qsparams.get('cmd', '')
98 if not mercurial.wireprotoserver.iscmd(cmd):
98 if not mercurial.wireprotoserver.iscmd(cmd):
99 # NOTE(marcink): for unsupported commands, we return bad request
99 # NOTE(marcink): for unsupported commands, we return bad request
100 # internally from HG
100 # internally from HG
101 from mercurial.hgweb.common import statusmessage
101 from mercurial.hgweb.common import statusmessage
102 res.status = statusmessage(mercurial.hgweb.common.HTTP_BAD_REQUEST)
102 res.status = statusmessage(mercurial.hgweb.common.HTTP_BAD_REQUEST)
103 res.setbodybytes('')
103 res.setbodybytes('')
104 return res.sendresponse()
104 return res.sendresponse()
105
105
106 return super(HgWeb, self)._runwsgi(req, res, repo)
106 return super(HgWeb, self)._runwsgi(req, res, repo)
107
107
108
108
109 def make_hg_ui_from_config(repo_config):
109 def make_hg_ui_from_config(repo_config):
110 baseui = mercurial.ui.ui()
110 baseui = mercurial.ui.ui()
111
111
112 # clean the baseui object
112 # clean the baseui object
113 baseui._ocfg = mercurial.config.config()
113 baseui._ocfg = mercurial.config.config()
114 baseui._ucfg = mercurial.config.config()
114 baseui._ucfg = mercurial.config.config()
115 baseui._tcfg = mercurial.config.config()
115 baseui._tcfg = mercurial.config.config()
116
116
117 for section, option, value in repo_config:
117 for section, option, value in repo_config:
118 baseui.setconfig(section, option, value)
118 baseui.setconfig(ascii_bytes(section), ascii_bytes(option), ascii_bytes(value))
119
119
120 # make our hgweb quiet so it doesn't print output
120 # make our hgweb quiet so it doesn't print output
121 baseui.setconfig('ui', 'quiet', 'true')
121 baseui.setconfig(b'ui', b'quiet', b'true')
122
122
123 return baseui
123 return baseui
124
124
125
125
126 def update_hg_ui_from_hgrc(baseui, repo_path):
126 def update_hg_ui_from_hgrc(baseui, repo_path):
127 path = os.path.join(repo_path, '.hg', 'hgrc')
127 path = os.path.join(repo_path, '.hg', 'hgrc')
128
128
129 if not os.path.isfile(path):
129 if not os.path.isfile(path):
130 log.debug('hgrc file is not present at %s, skipping...', path)
130 log.debug('hgrc file is not present at %s, skipping...', path)
131 return
131 return
132 log.debug('reading hgrc from %s', path)
132 log.debug('reading hgrc from %s', path)
133 cfg = mercurial.config.config()
133 cfg = mercurial.config.config()
134 cfg.read(path)
134 cfg.read(path)
135 for section in HG_UI_SECTIONS:
135 for section in HG_UI_SECTIONS:
136 for k, v in cfg.items(section):
136 for k, v in cfg.items(section):
137 log.debug('settings ui from file: [%s] %s=%s', section, k, v)
137 log.debug('settings ui from file: [%s] %s=%s', section, k, v)
138 baseui.setconfig(section, k, v)
138 baseui.setconfig(ascii_bytes(section), ascii_bytes(k), ascii_bytes(v))
139
139
140
140
141 def create_hg_wsgi_app(repo_path, repo_name, config):
141 def create_hg_wsgi_app(repo_path, repo_name, config):
142 """
142 """
143 Prepares a WSGI application to handle Mercurial requests.
143 Prepares a WSGI application to handle Mercurial requests.
144
144
145 :param config: is a list of 3-item tuples representing a ConfigObject
145 :param config: is a list of 3-item tuples representing a ConfigObject
146 (it is the serialized version of the config object).
146 (it is the serialized version of the config object).
147 """
147 """
148 log.debug("Creating Mercurial WSGI application")
148 log.debug("Creating Mercurial WSGI application")
149
149
150 baseui = make_hg_ui_from_config(config)
150 baseui = make_hg_ui_from_config(config)
151 update_hg_ui_from_hgrc(baseui, repo_path)
151 update_hg_ui_from_hgrc(baseui, repo_path)
152
152
153 try:
153 try:
154 return HgWeb(repo_path, name=repo_name, baseui=baseui)
154 return HgWeb(repo_path, name=repo_name, baseui=baseui)
155 except mercurial.error.RequirementError as e:
155 except mercurial.error.RequirementError as e:
156 raise exceptions.RequirementException(e)(e)
156 raise exceptions.RequirementException(e)(e)
157
157
158
158
159 class GitHandler(object):
159 class GitHandler(object):
160 """
160 """
161 Handler for Git operations like push/pull etc
161 Handler for Git operations like push/pull etc
162 """
162 """
163 def __init__(self, repo_location, repo_name, git_path, update_server_info,
163 def __init__(self, repo_location, repo_name, git_path, update_server_info,
164 extras):
164 extras):
165 if not os.path.isdir(repo_location):
165 if not os.path.isdir(repo_location):
166 raise OSError(repo_location)
166 raise OSError(repo_location)
167 self.content_path = repo_location
167 self.content_path = repo_location
168 self.repo_name = repo_name
168 self.repo_name = repo_name
169 self.repo_location = repo_location
169 self.repo_location = repo_location
170 self.extras = extras
170 self.extras = extras
171 self.git_path = git_path
171 self.git_path = git_path
172 self.update_server_info = update_server_info
172 self.update_server_info = update_server_info
173
173
174 def __call__(self, environ, start_response):
174 def __call__(self, environ, start_response):
175 app = webob.exc.HTTPNotFound()
175 app = webob.exc.HTTPNotFound()
176 candidate_paths = (
176 candidate_paths = (
177 self.content_path, os.path.join(self.content_path, '.git'))
177 self.content_path, os.path.join(self.content_path, '.git'))
178
178
179 for content_path in candidate_paths:
179 for content_path in candidate_paths:
180 try:
180 try:
181 app = pygrack.GitRepository(
181 app = pygrack.GitRepository(
182 self.repo_name, content_path, self.git_path,
182 self.repo_name, content_path, self.git_path,
183 self.update_server_info, self.extras)
183 self.update_server_info, self.extras)
184 break
184 break
185 except OSError:
185 except OSError:
186 continue
186 continue
187
187
188 return app(environ, start_response)
188 return app(environ, start_response)
189
189
190
190
191 def create_git_wsgi_app(repo_path, repo_name, config):
191 def create_git_wsgi_app(repo_path, repo_name, config):
192 """
192 """
193 Creates a WSGI application to handle Git requests.
193 Creates a WSGI application to handle Git requests.
194
194
195 :param config: is a dictionary holding the extras.
195 :param config: is a dictionary holding the extras.
196 """
196 """
197 git_path = settings.GIT_EXECUTABLE
197 git_path = settings.GIT_EXECUTABLE
198 update_server_info = config.pop('git_update_server_info')
198 update_server_info = config.pop('git_update_server_info')
199 app = GitHandler(
199 app = GitHandler(
200 repo_path, repo_name, git_path, update_server_info, config)
200 repo_path, repo_name, git_path, update_server_info, config)
201
201
202 return app
202 return app
203
203
204
204
205 class GitLFSHandler(object):
205 class GitLFSHandler(object):
206 """
206 """
207 Handler for Git LFS operations
207 Handler for Git LFS operations
208 """
208 """
209
209
210 def __init__(self, repo_location, repo_name, git_path, update_server_info,
210 def __init__(self, repo_location, repo_name, git_path, update_server_info,
211 extras):
211 extras):
212 if not os.path.isdir(repo_location):
212 if not os.path.isdir(repo_location):
213 raise OSError(repo_location)
213 raise OSError(repo_location)
214 self.content_path = repo_location
214 self.content_path = repo_location
215 self.repo_name = repo_name
215 self.repo_name = repo_name
216 self.repo_location = repo_location
216 self.repo_location = repo_location
217 self.extras = extras
217 self.extras = extras
218 self.git_path = git_path
218 self.git_path = git_path
219 self.update_server_info = update_server_info
219 self.update_server_info = update_server_info
220
220
221 def get_app(self, git_lfs_enabled, git_lfs_store_path, git_lfs_http_scheme):
221 def get_app(self, git_lfs_enabled, git_lfs_store_path, git_lfs_http_scheme):
222 app = git_lfs.create_app(git_lfs_enabled, git_lfs_store_path, git_lfs_http_scheme)
222 app = git_lfs.create_app(git_lfs_enabled, git_lfs_store_path, git_lfs_http_scheme)
223 return app
223 return app
224
224
225
225
226 def create_git_lfs_wsgi_app(repo_path, repo_name, config):
226 def create_git_lfs_wsgi_app(repo_path, repo_name, config):
227 git_path = settings.GIT_EXECUTABLE
227 git_path = settings.GIT_EXECUTABLE
228 update_server_info = config.pop('git_update_server_info')
228 update_server_info = config.pop(b'git_update_server_info')
229 git_lfs_enabled = config.pop('git_lfs_enabled')
229 git_lfs_enabled = config.pop(b'git_lfs_enabled')
230 git_lfs_store_path = config.pop('git_lfs_store_path')
230 git_lfs_store_path = config.pop(b'git_lfs_store_path')
231 git_lfs_http_scheme = config.pop('git_lfs_http_scheme', 'http')
231 git_lfs_http_scheme = config.pop(b'git_lfs_http_scheme', 'http')
232 app = GitLFSHandler(
232 app = GitLFSHandler(
233 repo_path, repo_name, git_path, update_server_info, config)
233 repo_path, repo_name, git_path, update_server_info, config)
234
234
235 return app.get_app(git_lfs_enabled, git_lfs_store_path, git_lfs_http_scheme)
235 return app.get_app(git_lfs_enabled, git_lfs_store_path, git_lfs_http_scheme)
@@ -1,519 +1,561 b''
1 """
1 """
2 Module provides a class allowing to wrap communication over subprocess.Popen
2 Module provides a class allowing to wrap communication over subprocess.Popen
3 input, output, error streams into a meaningfull, non-blocking, concurrent
3 input, output, error streams into a meaningfull, non-blocking, concurrent
4 stream processor exposing the output data as an iterator fitting to be a
4 stream processor exposing the output data as an iterator fitting to be a
5 return value passed by a WSGI applicaiton to a WSGI server per PEP 3333.
5 return value passed by a WSGI applicaiton to a WSGI server per PEP 3333.
6
6
7 Copyright (c) 2011 Daniel Dotsenko <dotsa[at]hotmail.com>
7 Copyright (c) 2011 Daniel Dotsenko <dotsa[at]hotmail.com>
8
8
9 This file is part of git_http_backend.py Project.
9 This file is part of git_http_backend.py Project.
10
10
11 git_http_backend.py Project is free software: you can redistribute it and/or
11 git_http_backend.py Project is free software: you can redistribute it and/or
12 modify it under the terms of the GNU Lesser General Public License as
12 modify it under the terms of the GNU Lesser General Public License as
13 published by the Free Software Foundation, either version 2.1 of the License,
13 published by the Free Software Foundation, either version 2.1 of the License,
14 or (at your option) any later version.
14 or (at your option) any later version.
15
15
16 git_http_backend.py Project is distributed in the hope that it will be useful,
16 git_http_backend.py Project is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU Lesser General Public License for more details.
19 GNU Lesser General Public License for more details.
20
20
21 You should have received a copy of the GNU Lesser General Public License
21 You should have received a copy of the GNU Lesser General Public License
22 along with git_http_backend.py Project.
22 along with git_http_backend.py Project.
23 If not, see <http://www.gnu.org/licenses/>.
23 If not, see <http://www.gnu.org/licenses/>.
24 """
24 """
25 import os
25 import os
26 import collections
26 import logging
27 import logging
27 import subprocess
28 import subprocess
28 from collections import deque
29 import threading
29 from threading import Event, Thread
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 class StreamFeeder(Thread):
34 class StreamFeeder(threading.Thread):
35 """
35 """
36 Normal writing into pipe-like is blocking once the buffer is filled.
36 Normal writing into pipe-like is blocking once the buffer is filled.
37 This thread allows a thread to seep data from a file-like into a pipe
37 This thread allows a thread to seep data from a file-like into a pipe
38 without blocking the main thread.
38 without blocking the main thread.
39 We close inpipe once the end of the source stream is reached.
39 We close inpipe once the end of the source stream is reached.
40 """
40 """
41
41
42 def __init__(self, source):
42 def __init__(self, source):
43 super(StreamFeeder, self).__init__()
43 super(StreamFeeder, self).__init__()
44 self.daemon = True
44 self.daemon = True
45 filelike = False
45 filelike = False
46 self.bytes = bytes()
46 self.bytes = bytes()
47 if type(source) in (type(''), bytes, bytearray): # string-like
47 if type(source) in (type(''), bytes, bytearray): # string-like
48 self.bytes = bytes(source)
48 self.bytes = bytes(source)
49 else: # can be either file pointer or file-like
49 else: # can be either file pointer or file-like
50 if type(source) in (int, int): # file pointer it is
50 if isinstance(source, int): # file pointer it is
51 # converting file descriptor (int) stdin into file-like
51 # converting file descriptor (int) stdin into file-like
52 try:
52 source = os.fdopen(source, 'rb', 16384)
53 source = os.fdopen(source, 'rb', 16384)
54 except Exception:
55 pass
56 # let's see if source is file-like by now
53 # let's see if source is file-like by now
57 try:
54 filelike = hasattr(source, 'read')
58 filelike = source.read
59 except Exception:
60 pass
61 if not filelike and not self.bytes:
55 if not filelike and not self.bytes:
62 raise TypeError("StreamFeeder's source object must be a readable "
56 raise TypeError("StreamFeeder's source object must be a readable "
63 "file-like, a file descriptor, or a string-like.")
57 "file-like, a file descriptor, or a string-like.")
64 self.source = source
58 self.source = source
65 self.readiface, self.writeiface = os.pipe()
59 self.readiface, self.writeiface = os.pipe()
66
60
67 def run(self):
61 def run(self):
68 t = self.writeiface
62 writer = self.writeiface
69 try:
63 try:
70 if self.bytes:
64 if self.bytes:
71 os.write(t, self.bytes)
65 os.write(writer, self.bytes)
72 else:
66 else:
73 s = self.source
67 s = self.source
74 b = s.read(4096)
68
75 while b:
69 while 1:
76 os.write(t, b)
70 _bytes = s.read(4096)
77 b = s.read(4096)
71 if not _bytes:
72 break
73 os.write(writer, _bytes)
74
78 finally:
75 finally:
79 os.close(t)
76 os.close(writer)
80
77
81 @property
78 @property
82 def output(self):
79 def output(self):
83 return self.readiface
80 return self.readiface
84
81
85
82
86 class InputStreamChunker(Thread):
83 class InputStreamChunker(threading.Thread):
87 def __init__(self, source, target, buffer_size, chunk_size):
84 def __init__(self, source, target, buffer_size, chunk_size):
88
85
89 super(InputStreamChunker, self).__init__()
86 super(InputStreamChunker, self).__init__()
90
87
91 self.daemon = True # die die die.
88 self.daemon = True # die die die.
92
89
93 self.source = source
90 self.source = source
94 self.target = target
91 self.target = target
95 self.chunk_count_max = int(buffer_size / chunk_size) + 1
92 self.chunk_count_max = int(buffer_size / chunk_size) + 1
96 self.chunk_size = chunk_size
93 self.chunk_size = chunk_size
97
94
98 self.data_added = Event()
95 self.data_added = threading.Event()
99 self.data_added.clear()
96 self.data_added.clear()
100
97
101 self.keep_reading = Event()
98 self.keep_reading = threading.Event()
102 self.keep_reading.set()
99 self.keep_reading.set()
103
100
104 self.EOF = Event()
101 self.EOF = threading.Event()
105 self.EOF.clear()
102 self.EOF.clear()
106
103
107 self.go = Event()
104 self.go = threading.Event()
108 self.go.set()
105 self.go.set()
109
106
110 def stop(self):
107 def stop(self):
111 self.go.clear()
108 self.go.clear()
112 self.EOF.set()
109 self.EOF.set()
113 try:
110 try:
114 # this is not proper, but is done to force the reader thread let
111 # this is not proper, but is done to force the reader thread let
115 # go of the input because, if successful, .close() will send EOF
112 # go of the input because, if successful, .close() will send EOF
116 # down the pipe.
113 # down the pipe.
117 self.source.close()
114 self.source.close()
118 except:
115 except:
119 pass
116 pass
120
117
121 def run(self):
118 def run(self):
122 s = self.source
119 s = self.source
123 t = self.target
120 t = self.target
124 cs = self.chunk_size
121 cs = self.chunk_size
125 chunk_count_max = self.chunk_count_max
122 chunk_count_max = self.chunk_count_max
126 keep_reading = self.keep_reading
123 keep_reading = self.keep_reading
127 da = self.data_added
124 da = self.data_added
128 go = self.go
125 go = self.go
129
126
130 try:
127 try:
131 b = s.read(cs)
128 b = s.read(cs)
132 except ValueError:
129 except ValueError:
133 b = ''
130 b = ''
134
131
135 timeout_input = 20
132 timeout_input = 20
136 while b and go.is_set():
133 while b and go.is_set():
137 if len(t) > chunk_count_max:
134 if len(t) > chunk_count_max:
138 keep_reading.clear()
135 keep_reading.clear()
139 keep_reading.wait(timeout_input)
136 keep_reading.wait(timeout_input)
140 if len(t) > chunk_count_max + timeout_input:
137 if len(t) > chunk_count_max + timeout_input:
141 log.error("Timed out while waiting for input from subprocess.")
138 log.error("Timed out while waiting for input from subprocess.")
142 os._exit(-1) # this will cause the worker to recycle itself
139 os._exit(-1) # this will cause the worker to recycle itself
143
140
144 t.append(b)
141 t.append(b)
145 da.set()
142 da.set()
146
143
147 try:
144 try:
148 b = s.read(cs)
145 b = s.read(cs)
149 except ValueError:
146 except ValueError: # probably "I/O operation on closed file"
150 b = ''
147 b = ''
151
148
152 self.EOF.set()
149 self.EOF.set()
153 da.set() # for cases when done but there was no input.
150 da.set() # for cases when done but there was no input.
154
151
155
152
156 class BufferedGenerator(object):
153 class BufferedGenerator(object):
157 """
154 """
158 Class behaves as a non-blocking, buffered pipe reader.
155 Class behaves as a non-blocking, buffered pipe reader.
159 Reads chunks of data (through a thread)
156 Reads chunks of data (through a thread)
160 from a blocking pipe, and attaches these to an array (Deque) of chunks.
157 from a blocking pipe, and attaches these to an array (Deque) of chunks.
161 Reading is halted in the thread when max chunks is internally buffered.
158 Reading is halted in the thread when max chunks is internally buffered.
162 The .next() may operate in blocking or non-blocking fashion by yielding
159 The .next() may operate in blocking or non-blocking fashion by yielding
163 '' if no data is ready
160 '' if no data is ready
164 to be sent or by not returning until there is some data to send
161 to be sent or by not returning until there is some data to send
165 When we get EOF from underlying source pipe we raise the marker to raise
162 When we get EOF from underlying source pipe we raise the marker to raise
166 StopIteration after the last chunk of data is yielded.
163 StopIteration after the last chunk of data is yielded.
167 """
164 """
168
165
169 def __init__(self, source, buffer_size=65536, chunk_size=4096,
166 def __init__(self, name, source, buffer_size=65536, chunk_size=4096,
170 starting_values=None, bottomless=False):
167 starting_values=None, bottomless=False):
171 starting_values = starting_values or []
168 starting_values = starting_values or []
169 self.name = name
170 self.buffer_size = buffer_size
171 self.chunk_size = chunk_size
172
172
173 if bottomless:
173 if bottomless:
174 maxlen = int(buffer_size / chunk_size)
174 maxlen = int(buffer_size / chunk_size)
175 else:
175 else:
176 maxlen = None
176 maxlen = None
177
177
178 self.data = deque(starting_values, maxlen)
178 self.data_queue = collections.deque(starting_values, maxlen)
179 self.worker = InputStreamChunker(source, self.data, buffer_size,
179 self.worker = InputStreamChunker(source, self.data_queue, buffer_size, chunk_size)
180 chunk_size)
181 if starting_values:
180 if starting_values:
182 self.worker.data_added.set()
181 self.worker.data_added.set()
183 self.worker.start()
182 self.worker.start()
184
183
185 ####################
184 ####################
186 # Generator's methods
185 # Generator's methods
187 ####################
186 ####################
187 def __str__(self):
188 return f'BufferedGenerator(name={self.name} chunk: {self.chunk_size} on buffer: {self.buffer_size})'
188
189
189 def __iter__(self):
190 def __iter__(self):
190 return self
191 return self
191
192
192 def __next__(self):
193 def __next__(self):
193 while not len(self.data) and not self.worker.EOF.is_set():
194
195 while not self.length and not self.worker.EOF.is_set():
194 self.worker.data_added.clear()
196 self.worker.data_added.clear()
195 self.worker.data_added.wait(0.2)
197 self.worker.data_added.wait(0.2)
196 if len(self.data):
198
199 if self.length:
197 self.worker.keep_reading.set()
200 self.worker.keep_reading.set()
198 return bytes(self.data.popleft())
201 return bytes(self.data_queue.popleft())
199 elif self.worker.EOF.is_set():
202 elif self.worker.EOF.is_set():
200 raise StopIteration
203 raise StopIteration
201
204
202 def throw(self, exc_type, value=None, traceback=None):
205 def throw(self, exc_type, value=None, traceback=None):
203 if not self.worker.EOF.is_set():
206 if not self.worker.EOF.is_set():
204 raise exc_type(value)
207 raise exc_type(value)
205
208
206 def start(self):
209 def start(self):
207 self.worker.start()
210 self.worker.start()
208
211
209 def stop(self):
212 def stop(self):
210 self.worker.stop()
213 self.worker.stop()
211
214
212 def close(self):
215 def close(self):
213 try:
216 try:
214 self.worker.stop()
217 self.worker.stop()
215 self.throw(GeneratorExit)
218 self.throw(GeneratorExit)
216 except (GeneratorExit, StopIteration):
219 except (GeneratorExit, StopIteration):
217 pass
220 pass
218
221
219 ####################
222 ####################
220 # Threaded reader's infrastructure.
223 # Threaded reader's infrastructure.
221 ####################
224 ####################
222 @property
225 @property
223 def input(self):
226 def input(self):
224 return self.worker.w
227 return self.worker.w
225
228
226 @property
229 @property
227 def data_added_event(self):
230 def data_added_event(self):
228 return self.worker.data_added
231 return self.worker.data_added
229
232
230 @property
233 @property
231 def data_added(self):
234 def data_added(self):
232 return self.worker.data_added.is_set()
235 return self.worker.data_added.is_set()
233
236
234 @property
237 @property
235 def reading_paused(self):
238 def reading_paused(self):
236 return not self.worker.keep_reading.is_set()
239 return not self.worker.keep_reading.is_set()
237
240
238 @property
241 @property
239 def done_reading_event(self):
242 def done_reading_event(self):
240 """
243 """
241 Done_reding does not mean that the iterator's buffer is empty.
244 Done_reding does not mean that the iterator's buffer is empty.
242 Iterator might have done reading from underlying source, but the read
245 Iterator might have done reading from underlying source, but the read
243 chunks might still be available for serving through .next() method.
246 chunks might still be available for serving through .next() method.
244
247
245 :returns: An Event class instance.
248 :returns: An Event class instance.
246 """
249 """
247 return self.worker.EOF
250 return self.worker.EOF
248
251
249 @property
252 @property
250 def done_reading(self):
253 def done_reading(self):
251 """
254 """
252 Done_reding does not mean that the iterator's buffer is empty.
255 Done_reading does not mean that the iterator's buffer is empty.
253 Iterator might have done reading from underlying source, but the read
256 Iterator might have done reading from underlying source, but the read
254 chunks might still be available for serving through .next() method.
257 chunks might still be available for serving through .next() method.
255
258
256 :returns: An Bool value.
259 :returns: An Bool value.
257 """
260 """
258 return self.worker.EOF.is_set()
261 return self.worker.EOF.is_set()
259
262
260 @property
263 @property
261 def length(self):
264 def length(self):
262 """
265 """
263 returns int.
266 returns int.
264
267
265 This is the lenght of the que of chunks, not the length of
268 This is the length of the queue of chunks, not the length of
266 the combined contents in those chunks.
269 the combined contents in those chunks.
267
270
268 __len__() cannot be meaningfully implemented because this
271 __len__() cannot be meaningfully implemented because this
269 reader is just flying throuh a bottomless pit content and
272 reader is just flying through a bottomless pit content and
270 can only know the lenght of what it already saw.
273 can only know the length of what it already saw.
271
274
272 If __len__() on WSGI server per PEP 3333 returns a value,
275 If __len__() on WSGI server per PEP 3333 returns a value,
273 the responce's length will be set to that. In order not to
276 the response's length will be set to that. In order not to
274 confuse WSGI PEP3333 servers, we will not implement __len__
277 confuse WSGI PEP3333 servers, we will not implement __len__
275 at all.
278 at all.
276 """
279 """
277 return len(self.data)
280 return len(self.data_queue)
278
281
279 def prepend(self, x):
282 def prepend(self, x):
280 self.data.appendleft(x)
283 self.data_queue.appendleft(x)
281
284
282 def append(self, x):
285 def append(self, x):
283 self.data.append(x)
286 self.data_queue.append(x)
284
287
285 def extend(self, o):
288 def extend(self, o):
286 self.data.extend(o)
289 self.data_queue.extend(o)
287
290
288 def __getitem__(self, i):
291 def __getitem__(self, i):
289 return self.data[i]
292 return self.data_queue[i]
290
293
291
294
292 class SubprocessIOChunker(object):
295 class SubprocessIOChunker(object):
293 """
296 """
294 Processor class wrapping handling of subprocess IO.
297 Processor class wrapping handling of subprocess IO.
295
298
296 .. important::
299 .. important::
297
300
298 Watch out for the method `__del__` on this class. If this object
301 Watch out for the method `__del__` on this class. If this object
299 is deleted, it will kill the subprocess, so avoid to
302 is deleted, it will kill the subprocess, so avoid to
300 return the `output` attribute or usage of it like in the following
303 return the `output` attribute or usage of it like in the following
301 example::
304 example::
302
305
303 # `args` expected to run a program that produces a lot of output
306 # `args` expected to run a program that produces a lot of output
304 output = ''.join(SubprocessIOChunker(
307 output = ''.join(SubprocessIOChunker(
305 args, shell=False, inputstream=inputstream, env=environ).output)
308 args, shell=False, inputstream=inputstream, env=environ).output)
306
309
307 # `output` will not contain all the data, because the __del__ method
310 # `output` will not contain all the data, because the __del__ method
308 # has already killed the subprocess in this case before all output
311 # has already killed the subprocess in this case before all output
309 # has been consumed.
312 # has been consumed.
310
313
311
314
312
315
313 In a way, this is a "communicate()" replacement with a twist.
316 In a way, this is a "communicate()" replacement with a twist.
314
317
315 - We are multithreaded. Writing in and reading out, err are all sep threads.
318 - We are multithreaded. Writing in and reading out, err are all sep threads.
316 - We support concurrent (in and out) stream processing.
319 - We support concurrent (in and out) stream processing.
317 - The output is not a stream. It's a queue of read string (bytes, not unicode)
320 - The output is not a stream. It's a queue of read string (bytes, not str)
318 chunks. The object behaves as an iterable. You can "for chunk in obj:" us.
321 chunks. The object behaves as an iterable. You can "for chunk in obj:" us.
319 - We are non-blocking in more respects than communicate()
322 - We are non-blocking in more respects than communicate()
320 (reading from subprocess out pauses when internal buffer is full, but
323 (reading from subprocess out pauses when internal buffer is full, but
321 does not block the parent calling code. On the flip side, reading from
324 does not block the parent calling code. On the flip side, reading from
322 slow-yielding subprocess may block the iteration until data shows up. This
325 slow-yielding subprocess may block the iteration until data shows up. This
323 does not block the parallel inpipe reading occurring parallel thread.)
326 does not block the parallel inpipe reading occurring parallel thread.)
324
327
325 The purpose of the object is to allow us to wrap subprocess interactions into
328 The purpose of the object is to allow us to wrap subprocess interactions into
326 and interable that can be passed to a WSGI server as the application's return
329 an iterable that can be passed to a WSGI server as the application's return
327 value. Because of stream-processing-ability, WSGI does not have to read ALL
330 value. Because of stream-processing-ability, WSGI does not have to read ALL
328 of the subprocess's output and buffer it, before handing it to WSGI server for
331 of the subprocess's output and buffer it, before handing it to WSGI server for
329 HTTP response. Instead, the class initializer reads just a bit of the stream
332 HTTP response. Instead, the class initializer reads just a bit of the stream
330 to figure out if error ocurred or likely to occur and if not, just hands the
333 to figure out if error occurred or likely to occur and if not, just hands the
331 further iteration over subprocess output to the server for completion of HTTP
334 further iteration over subprocess output to the server for completion of HTTP
332 response.
335 response.
333
336
334 The real or perceived subprocess error is trapped and raised as one of
337 The real or perceived subprocess error is trapped and raised as one of
335 EnvironmentError family of exceptions
338 OSError family of exceptions
336
339
337 Example usage:
340 Example usage:
338 # try:
341 # try:
339 # answer = SubprocessIOChunker(
342 # answer = SubprocessIOChunker(
340 # cmd,
343 # cmd,
341 # input,
344 # input,
342 # buffer_size = 65536,
345 # buffer_size = 65536,
343 # chunk_size = 4096
346 # chunk_size = 4096
344 # )
347 # )
345 # except (EnvironmentError) as e:
348 # except (OSError) as e:
346 # print str(e)
349 # print str(e)
347 # raise e
350 # raise e
348 #
351 #
349 # return answer
352 # return answer
350
353
351
354
352 """
355 """
353
356
354 # TODO: johbo: This is used to make sure that the open end of the PIPE
357 # TODO: johbo: This is used to make sure that the open end of the PIPE
355 # is closed in the end. It would be way better to wrap this into an
358 # is closed in the end. It would be way better to wrap this into an
356 # object, so that it is closed automatically once it is consumed or
359 # object, so that it is closed automatically once it is consumed or
357 # something similar.
360 # something similar.
358 _close_input_fd = None
361 _close_input_fd = None
359
362
360 _closed = False
363 _closed = False
364 _stdout = None
365 _stderr = None
361
366
362 def __init__(self, cmd, inputstream=None, buffer_size=65536,
367 def __init__(self, cmd, input_stream=None, buffer_size=65536,
363 chunk_size=4096, starting_values=None, fail_on_stderr=True,
368 chunk_size=4096, starting_values=None, fail_on_stderr=True,
364 fail_on_return_code=True, **kwargs):
369 fail_on_return_code=True, **kwargs):
365 """
370 """
366 Initializes SubprocessIOChunker
371 Initializes SubprocessIOChunker
367
372
368 :param cmd: A Subprocess.Popen style "cmd". Can be string or array of strings
373 :param cmd: A Subprocess.Popen style "cmd". Can be string or array of strings
369 :param inputstream: (Default: None) A file-like, string, or file pointer.
374 :param input_stream: (Default: None) A file-like, string, or file pointer.
370 :param buffer_size: (Default: 65536) A size of total buffer per stream in bytes.
375 :param buffer_size: (Default: 65536) A size of total buffer per stream in bytes.
371 :param chunk_size: (Default: 4096) A max size of a chunk. Actual chunk may be smaller.
376 :param chunk_size: (Default: 4096) A max size of a chunk. Actual chunk may be smaller.
372 :param starting_values: (Default: []) An array of strings to put in front of output que.
377 :param starting_values: (Default: []) An array of strings to put in front of output que.
373 :param fail_on_stderr: (Default: True) Whether to raise an exception in
378 :param fail_on_stderr: (Default: True) Whether to raise an exception in
374 case something is written to stderr.
379 case something is written to stderr.
375 :param fail_on_return_code: (Default: True) Whether to raise an
380 :param fail_on_return_code: (Default: True) Whether to raise an
376 exception if the return code is not 0.
381 exception if the return code is not 0.
377 """
382 """
378
383
384 kwargs['shell'] = kwargs.get('shell', True)
385
379 starting_values = starting_values or []
386 starting_values = starting_values or []
380 if inputstream:
387 if input_stream:
381 input_streamer = StreamFeeder(inputstream)
388 input_streamer = StreamFeeder(input_stream)
382 input_streamer.start()
389 input_streamer.start()
383 inputstream = input_streamer.output
390 input_stream = input_streamer.output
384 self._close_input_fd = inputstream
391 self._close_input_fd = input_stream
385
392
386 self._fail_on_stderr = fail_on_stderr
393 self._fail_on_stderr = fail_on_stderr
387 self._fail_on_return_code = fail_on_return_code
394 self._fail_on_return_code = fail_on_return_code
388
395 self.cmd = cmd
389 _shell = kwargs.get('shell', True)
390 kwargs['shell'] = _shell
391
396
392 _p = subprocess.Popen(cmd, bufsize=-1,
397 _p = subprocess.Popen(cmd, bufsize=-1, stdin=input_stream, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
393 stdin=inputstream,
394 stdout=subprocess.PIPE,
395 stderr=subprocess.PIPE,
396 **kwargs)
398 **kwargs)
399 self.process = _p
397
400
398 bg_out = BufferedGenerator(_p.stdout, buffer_size, chunk_size,
401 bg_out = BufferedGenerator('stdout', _p.stdout, buffer_size, chunk_size, starting_values)
399 starting_values)
402 bg_err = BufferedGenerator('stderr', _p.stderr, 10240, 1, bottomless=True)
400 bg_err = BufferedGenerator(_p.stderr, 16000, 1, bottomless=True)
401
403
402 while not bg_out.done_reading and not bg_out.reading_paused and not bg_err.length:
404 while not bg_out.done_reading and not bg_out.reading_paused and not bg_err.length:
403 # doing this until we reach either end of file, or end of buffer.
405 # doing this until we reach either end of file, or end of buffer.
404 bg_out.data_added_event.wait(1)
406 bg_out.data_added_event.wait(0.2)
405 bg_out.data_added_event.clear()
407 bg_out.data_added_event.clear()
406
408
407 # at this point it's still ambiguous if we are done reading or just full buffer.
409 # at this point it's still ambiguous if we are done reading or just full buffer.
408 # Either way, if error (returned by ended process, or implied based on
410 # Either way, if error (returned by ended process, or implied based on
409 # presence of stuff in stderr output) we error out.
411 # presence of stuff in stderr output) we error out.
410 # Else, we are happy.
412 # Else, we are happy.
411 _returncode = _p.poll()
413 return_code = _p.poll()
414 ret_code_ok = return_code in [None, 0]
415 ret_code_fail = return_code is not None and return_code != 0
416 if (
417 (ret_code_fail and fail_on_return_code) or
418 (ret_code_ok and fail_on_stderr and bg_err.length)
419 ):
412
420
413 if ((_returncode and fail_on_return_code) or
414 (fail_on_stderr and _returncode is None and bg_err.length)):
415 try:
421 try:
416 _p.terminate()
422 _p.terminate()
417 except Exception:
423 except Exception:
418 pass
424 pass
425
419 bg_out.stop()
426 bg_out.stop()
427 out = b''.join(bg_out)
428 self._stdout = out
429
420 bg_err.stop()
430 bg_err.stop()
421 if fail_on_stderr:
431 err = b''.join(bg_err)
422 err = ''.join(bg_err)
432 self._stderr = err
423 raise EnvironmentError(
433
424 "Subprocess exited due to an error:\n" + err)
434 # code from https://github.com/schacon/grack/pull/7
425 if _returncode and fail_on_return_code:
435 if err.strip() == b'fatal: The remote end hung up unexpectedly' and out.startswith(b'0034shallow '):
426 err = ''.join(bg_err)
436 bg_out = iter([out])
437 _p = None
438 elif err and fail_on_stderr:
439 text_err = err.decode()
440 raise OSError(
441 "Subprocess exited due to an error:\n{}".format(text_err))
442
443 if ret_code_fail and fail_on_return_code:
444 text_err = err.decode()
427 if not err:
445 if not err:
428 # maybe get empty stderr, try stdout instead
446 # maybe get empty stderr, try stdout instead
429 # in many cases git reports the errors on stdout too
447 # in many cases git reports the errors on stdout too
430 err = ''.join(bg_out)
448 text_err = out.decode()
431 raise EnvironmentError(
449 raise OSError(
432 "Subprocess exited with non 0 ret code:%s: stderr:%s" % (
450 "Subprocess exited with non 0 ret code:{}: stderr:{}".format(return_code, text_err))
433 _returncode, err))
434
451
435 self.process = _p
452 self.stdout = bg_out
436 self.output = bg_out
453 self.stderr = bg_err
437 self.error = bg_err
454 self.inputstream = input_stream
438 self.inputstream = inputstream
455
456 def __str__(self):
457 proc = getattr(self, 'process', 'NO_PROCESS')
458 return f'SubprocessIOChunker: {proc}'
439
459
440 def __iter__(self):
460 def __iter__(self):
441 return self
461 return self
442
462
443 def __next__(self):
463 def __next__(self):
444 # Note: mikhail: We need to be sure that we are checking the return
464 # Note: mikhail: We need to be sure that we are checking the return
445 # code after the stdout stream is closed. Some processes, e.g. git
465 # code after the stdout stream is closed. Some processes, e.g. git
446 # are doing some magic in between closing stdout and terminating the
466 # are doing some magic in between closing stdout and terminating the
447 # process and, as a result, we are not getting return code on "slow"
467 # process and, as a result, we are not getting return code on "slow"
448 # systems.
468 # systems.
449 result = None
469 result = None
450 stop_iteration = None
470 stop_iteration = None
451 try:
471 try:
452 result = next(self.output)
472 result = next(self.stdout)
453 except StopIteration as e:
473 except StopIteration as e:
454 stop_iteration = e
474 stop_iteration = e
455
475
456 if self.process.poll() and self._fail_on_return_code:
476 if self.process:
457 err = '%s' % ''.join(self.error)
477 return_code = self.process.poll()
458 raise EnvironmentError(
478 ret_code_fail = return_code is not None and return_code != 0
459 "Subprocess exited due to an error:\n" + err)
479 if ret_code_fail and self._fail_on_return_code:
480 self.stop_streams()
481 err = self.get_stderr()
482 raise OSError(
483 "Subprocess exited (exit_code:{}) due to an error during iteration:\n{}".format(return_code, err))
460
484
461 if stop_iteration:
485 if stop_iteration:
462 raise stop_iteration
486 raise stop_iteration
463 return result
487 return result
464
488
465 def throw(self, type, value=None, traceback=None):
489 def throw(self, exc_type, value=None, traceback=None):
466 if self.output.length or not self.output.done_reading:
490 if self.stdout.length or not self.stdout.done_reading:
467 raise type(value)
491 raise exc_type(value)
468
492
469 def close(self):
493 def close(self):
470 if self._closed:
494 if self._closed:
471 return
495 return
472 self._closed = True
496
473 try:
497 try:
474 self.process.terminate()
498 self.process.terminate()
475 except Exception:
499 except Exception:
476 pass
500 pass
477 if self._close_input_fd:
501 if self._close_input_fd:
478 os.close(self._close_input_fd)
502 os.close(self._close_input_fd)
479 try:
503 try:
480 self.output.close()
504 self.stdout.close()
481 except Exception:
505 except Exception:
482 pass
506 pass
483 try:
507 try:
484 self.error.close()
508 self.stderr.close()
485 except Exception:
509 except Exception:
486 pass
510 pass
487 try:
511 try:
488 os.close(self.inputstream)
512 os.close(self.inputstream)
489 except Exception:
513 except Exception:
490 pass
514 pass
491
515
516 self._closed = True
517
518 def stop_streams(self):
519 getattr(self.stdout, 'stop', lambda: None)()
520 getattr(self.stderr, 'stop', lambda: None)()
521
522 def get_stdout(self):
523 if self._stdout:
524 return self._stdout
525 else:
526 return b''.join(self.stdout)
527
528 def get_stderr(self):
529 if self._stderr:
530 return self._stderr
531 else:
532 return b''.join(self.stderr)
533
492
534
493 def run_command(arguments, env=None):
535 def run_command(arguments, env=None):
494 """
536 """
495 Run the specified command and return the stdout.
537 Run the specified command and return the stdout.
496
538
497 :param arguments: sequence of program arguments (including the program name)
539 :param arguments: sequence of program arguments (including the program name)
498 :type arguments: list[str]
540 :type arguments: list[str]
499 """
541 """
500
542
501 cmd = arguments
543 cmd = arguments
502 log.debug('Running subprocessio command %s', cmd)
544 log.debug('Running subprocessio command %s', cmd)
503 proc = None
545 proc = None
504 try:
546 try:
505 _opts = {'shell': False, 'fail_on_stderr': False}
547 _opts = {'shell': False, 'fail_on_stderr': False}
506 if env:
548 if env:
507 _opts.update({'env': env})
549 _opts.update({'env': env})
508 proc = SubprocessIOChunker(cmd, **_opts)
550 proc = SubprocessIOChunker(cmd, **_opts)
509 return ''.join(proc), ''.join(proc.error)
551 return b''.join(proc), b''.join(proc.stderr)
510 except (EnvironmentError, OSError) as err:
552 except OSError as err:
511 cmd = ' '.join(cmd) # human friendly CMD
553 cmd = ' '.join(cmd) # human friendly CMD
512 tb_err = ("Couldn't run subprocessio command (%s).\n"
554 tb_err = ("Couldn't run subprocessio command (%s).\n"
513 "Original error was:%s\n" % (cmd, err))
555 "Original error was:%s\n" % (cmd, err))
514 log.exception(tb_err)
556 log.exception(tb_err)
515 raise Exception(tb_err)
557 raise Exception(tb_err)
516 finally:
558 finally:
517 if proc:
559 if proc:
518 proc.close()
560 proc.close()
519
561
@@ -1,159 +1,162 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import inspect
18 import inspect
19
19
20 import pytest
20 import pytest
21 import dulwich.errors
21 import dulwich.errors
22 from mock import Mock, patch
22 from mock import Mock, patch
23
23
24 from vcsserver.remote import git
24 from vcsserver.remote import git
25
25
26 SAMPLE_REFS = {
26 SAMPLE_REFS = {
27 'HEAD': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
27 'HEAD': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
28 'refs/tags/v0.1.9': '341d28f0eec5ddf0b6b77871e13c2bbd6bec685c',
28 'refs/tags/v0.1.9': '341d28f0eec5ddf0b6b77871e13c2bbd6bec685c',
29 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
29 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
30 'refs/tags/v0.1.1': 'e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0',
30 'refs/tags/v0.1.1': 'e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0',
31 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
31 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
32 }
32 }
33
33
34
34
35 @pytest.fixture
35 @pytest.fixture
36 def git_remote():
36 def git_remote():
37 """
37 """
38 A GitRemote instance with a mock factory.
38 A GitRemote instance with a mock factory.
39 """
39 """
40 factory = Mock()
40 factory = Mock()
41 remote = git.GitRemote(factory)
41 remote = git.GitRemote(factory)
42 return remote
42 return remote
43
43
44
44
45 def test_discover_git_version(git_remote):
45 def test_discover_git_version(git_remote):
46 version = git_remote.discover_git_version()
46 version = git_remote.discover_git_version()
47 assert version
47 assert version
48
48
49
49
50 class TestGitFetch(object):
50 class TestGitFetch(object):
51 def setup(self):
51 def setup_method(self):
52 self.mock_repo = Mock()
52 self.mock_repo = Mock()
53 factory = Mock()
53 factory = Mock()
54 factory.repo = Mock(return_value=self.mock_repo)
54 factory.repo = Mock(return_value=self.mock_repo)
55 self.remote_git = git.GitRemote(factory)
55 self.remote_git = git.GitRemote(factory)
56
56
57 def test_fetches_all_when_no_commit_ids_specified(self):
57 def test_fetches_all_when_no_commit_ids_specified(self):
58 def side_effect(determine_wants, *args, **kwargs):
58 def side_effect(determine_wants, *args, **kwargs):
59 determine_wants(SAMPLE_REFS)
59 determine_wants(SAMPLE_REFS)
60
60
61 with patch('dulwich.client.LocalGitClient.fetch') as mock_fetch:
61 with patch('dulwich.client.LocalGitClient.fetch') as mock_fetch:
62 mock_fetch.side_effect = side_effect
62 mock_fetch.side_effect = side_effect
63 self.remote_git.pull(wire={}, url='/tmp/', apply_refs=False)
63 self.remote_git.pull(wire={}, url='/tmp/', apply_refs=False)
64 determine_wants = self.mock_repo.object_store.determine_wants_all
64 determine_wants = self.mock_repo.object_store.determine_wants_all
65 determine_wants.assert_called_once_with(SAMPLE_REFS)
65 determine_wants.assert_called_once_with(SAMPLE_REFS)
66
66
67 def test_fetches_specified_commits(self):
67 def test_fetches_specified_commits(self):
68 selected_refs = {
68 selected_refs = {
69 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
69 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
70 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
70 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
71 }
71 }
72
72
73 def side_effect(determine_wants, *args, **kwargs):
73 def side_effect(determine_wants, *args, **kwargs):
74 result = determine_wants(SAMPLE_REFS)
74 result = determine_wants(SAMPLE_REFS)
75 assert sorted(result) == sorted(selected_refs.values())
75 assert sorted(result) == sorted(selected_refs.values())
76 return result
76 return result
77
77
78 with patch('dulwich.client.LocalGitClient.fetch') as mock_fetch:
78 with patch('dulwich.client.LocalGitClient.fetch') as mock_fetch:
79 mock_fetch.side_effect = side_effect
79 mock_fetch.side_effect = side_effect
80 self.remote_git.pull(
80 self.remote_git.pull(
81 wire={}, url='/tmp/', apply_refs=False,
81 wire={}, url='/tmp/', apply_refs=False,
82 refs=selected_refs.keys())
82 refs=selected_refs.keys())
83 determine_wants = self.mock_repo.object_store.determine_wants_all
83 determine_wants = self.mock_repo.object_store.determine_wants_all
84 assert determine_wants.call_count == 0
84 assert determine_wants.call_count == 0
85
85
86 def test_get_remote_refs(self):
86 def test_get_remote_refs(self):
87 factory = Mock()
87 factory = Mock()
88 remote_git = git.GitRemote(factory)
88 remote_git = git.GitRemote(factory)
89 url = 'http://example.com/test/test.git'
89 url = 'http://example.com/test/test.git'
90 sample_refs = {
90 sample_refs = {
91 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
91 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
92 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
92 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
93 }
93 }
94
94
95 with patch('vcsserver.git.Repo', create=False) as mock_repo:
95 with patch('vcsserver.remote.git.Repo', create=False) as mock_repo:
96 mock_repo().get_refs.return_value = sample_refs
96 mock_repo().get_refs.return_value = sample_refs
97 remote_refs = remote_git.get_remote_refs(wire={}, url=url)
97 remote_refs = remote_git.get_remote_refs(wire={}, url=url)
98 mock_repo().get_refs.assert_called_once_with()
98 mock_repo().get_refs.assert_called_once_with()
99 assert remote_refs == sample_refs
99 assert remote_refs == sample_refs
100
100
101
101
102 class TestReraiseSafeExceptions(object):
102 class TestReraiseSafeExceptions(object):
103
103
104 def test_method_decorated_with_reraise_safe_exceptions(self):
104 def test_method_decorated_with_reraise_safe_exceptions(self):
105 factory = Mock()
105 factory = Mock()
106 git_remote = git.GitRemote(factory)
106 git_remote = git.GitRemote(factory)
107
107
108 def fake_function():
108 def fake_function():
109 return None
109 return None
110
110
111 decorator = git.reraise_safe_exceptions(fake_function)
111 decorator = git.reraise_safe_exceptions(fake_function)
112
112
113 methods = inspect.getmembers(git_remote, predicate=inspect.ismethod)
113 methods = inspect.getmembers(git_remote, predicate=inspect.ismethod)
114 for method_name, method in methods:
114 for method_name, method in methods:
115 if not method_name.startswith('_') and method_name not in ['vcsserver_invalidate_cache']:
115 if not method_name.startswith('_') and method_name not in ['vcsserver_invalidate_cache']:
116 assert method.__func__.__code__ == decorator.__code__
116 assert method.__func__.__code__ == decorator.__code__
117
117
118 @pytest.mark.parametrize('side_effect, expected_type', [
118 @pytest.mark.parametrize('side_effect, expected_type', [
119 (dulwich.errors.ChecksumMismatch('0000000', 'deadbeef'), 'lookup'),
119 (dulwich.errors.ChecksumMismatch('0000000', 'deadbeef'), 'lookup'),
120 (dulwich.errors.NotCommitError('deadbeef'), 'lookup'),
120 (dulwich.errors.NotCommitError('deadbeef'), 'lookup'),
121 (dulwich.errors.MissingCommitError('deadbeef'), 'lookup'),
121 (dulwich.errors.MissingCommitError('deadbeef'), 'lookup'),
122 (dulwich.errors.ObjectMissing('deadbeef'), 'lookup'),
122 (dulwich.errors.ObjectMissing('deadbeef'), 'lookup'),
123 (dulwich.errors.HangupException(), 'error'),
123 (dulwich.errors.HangupException(), 'error'),
124 (dulwich.errors.UnexpectedCommandError('test-cmd'), 'error'),
124 (dulwich.errors.UnexpectedCommandError('test-cmd'), 'error'),
125 ])
125 ])
126 def test_safe_exceptions_reraised(self, side_effect, expected_type):
126 def test_safe_exceptions_reraised(self, side_effect, expected_type):
127 @git.reraise_safe_exceptions
127 @git.reraise_safe_exceptions
128 def fake_method():
128 def fake_method():
129 raise side_effect
129 raise side_effect
130
130
131 with pytest.raises(Exception) as exc_info:
131 with pytest.raises(Exception) as exc_info:
132 fake_method()
132 fake_method()
133 assert type(exc_info.value) == Exception
133 assert type(exc_info.value) == Exception
134 assert exc_info.value._vcs_kind == expected_type
134 assert exc_info.value._vcs_kind == expected_type
135
135
136
136
137 class TestDulwichRepoWrapper(object):
137 class TestDulwichRepoWrapper(object):
138 def test_calls_close_on_delete(self):
138 def test_calls_close_on_delete(self):
139 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
139 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
140 with isdir_patcher:
140 with patch.object(git.Repo, 'close') as close_mock:
141 repo = git.Repo('/tmp/abcde')
141 with isdir_patcher:
142 with patch.object(git.DulwichRepo, 'close') as close_mock:
142 repo = git.Repo('/tmp/abcde')
143 del repo
143 assert repo is not None
144 close_mock.assert_called_once_with()
144 repo.__del__()
145 # can't use del repo as in python3 this isn't always calling .__del__()
146
147 close_mock.assert_called_once_with()
145
148
146
149
147 class TestGitFactory(object):
150 class TestGitFactory(object):
148 def test_create_repo_returns_dulwich_wrapper(self):
151 def test_create_repo_returns_dulwich_wrapper(self):
149
152
150 with patch('vcsserver.lib.rc_cache.region_meta.dogpile_cache_regions') as mock:
153 with patch('vcsserver.lib.rc_cache.region_meta.dogpile_cache_regions') as mock:
151 mock.side_effect = {'repo_objects': ''}
154 mock.side_effect = {'repo_objects': ''}
152 factory = git.GitFactory()
155 factory = git.GitFactory()
153 wire = {
156 wire = {
154 'path': '/tmp/abcde'
157 'path': '/tmp/abcde'
155 }
158 }
156 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
159 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
157 with isdir_patcher:
160 with isdir_patcher:
158 result = factory._create_repo(wire, True)
161 result = factory._create_repo(wire, True)
159 assert isinstance(result, git.Repo)
162 assert isinstance(result, git.Repo)
@@ -1,109 +1,108 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import inspect
18 import inspect
19 import sys
19 import sys
20 import traceback
20 import traceback
21
21
22 import pytest
22 import pytest
23 from mercurial.error import LookupError
23 from mercurial.error import LookupError
24 from mock import Mock, patch
24 from mock import Mock, patch
25
25
26 from vcsserver import exceptions, hgcompat
26 from vcsserver import exceptions, hgcompat
27 from vcsserver.remote import hg
27 from vcsserver.remote import hg
28
28
29
29
30 class TestDiff(object):
30 class TestDiff(object):
31 def test_raising_safe_exception_when_lookup_failed(self):
31 def test_raising_safe_exception_when_lookup_failed(self):
32
32
33 factory = Mock()
33 factory = Mock()
34 hg_remote = hg.HgRemote(factory)
34 hg_remote = hg.HgRemote(factory)
35 with patch('mercurial.patch.diff') as diff_mock:
35 with patch('mercurial.patch.diff') as diff_mock:
36 diff_mock.side_effect = LookupError(
36 diff_mock.side_effect = LookupError(b'deadbeef', b'index', b'message')
37 'deadbeef', 'index', 'message')
37
38 with pytest.raises(Exception) as exc_info:
38 with pytest.raises(Exception) as exc_info:
39 hg_remote.diff(
39 hg_remote.diff(
40 wire={}, commit_id_1='deadbeef', commit_id_2='deadbee1',
40 wire={}, commit_id_1='deadbeef', commit_id_2='deadbee1',
41 file_filter=None, opt_git=True, opt_ignorews=True,
41 file_filter=None, opt_git=True, opt_ignorews=True,
42 context=3)
42 context=3)
43 assert type(exc_info.value) == Exception
43 assert type(exc_info.value) == Exception
44 assert exc_info.value._vcs_kind == 'lookup'
44 assert exc_info.value._vcs_kind == 'lookup'
45
45
46
46
47 class TestReraiseSafeExceptions(object):
47 class TestReraiseSafeExceptions(object):
48 def test_method_decorated_with_reraise_safe_exceptions(self):
48 def test_method_decorated_with_reraise_safe_exceptions(self):
49 factory = Mock()
49 factory = Mock()
50 hg_remote = hg.HgRemote(factory)
50 hg_remote = hg.HgRemote(factory)
51 methods = inspect.getmembers(hg_remote, predicate=inspect.ismethod)
51 methods = inspect.getmembers(hg_remote, predicate=inspect.ismethod)
52 decorator = hg.reraise_safe_exceptions(None)
52 decorator = hg.reraise_safe_exceptions(None)
53 for method_name, method in methods:
53 for method_name, method in methods:
54 if not method_name.startswith('_') and method_name not in ['vcsserver_invalidate_cache']:
54 if not method_name.startswith('_') and method_name not in ['vcsserver_invalidate_cache']:
55 assert method.__func__.__code__ == decorator.__code__
55 assert method.__func__.__code__ == decorator.__code__
56
56
57 @pytest.mark.parametrize('side_effect, expected_type', [
57 @pytest.mark.parametrize('side_effect, expected_type', [
58 (hgcompat.Abort(), 'abort'),
58 (hgcompat.Abort('failed-abort'), 'abort'),
59 (hgcompat.InterventionRequired(), 'abort'),
59 (hgcompat.InterventionRequired('intervention-required'), 'abort'),
60 (hgcompat.RepoLookupError(), 'lookup'),
60 (hgcompat.RepoLookupError(), 'lookup'),
61 (hgcompat.LookupError('deadbeef', 'index', 'message'), 'lookup'),
61 (hgcompat.LookupError(b'deadbeef', b'index', b'message'), 'lookup'),
62 (hgcompat.RepoError(), 'error'),
62 (hgcompat.RepoError(), 'error'),
63 (hgcompat.RequirementError(), 'requirement'),
63 (hgcompat.RequirementError(), 'requirement'),
64 ])
64 ])
65 def test_safe_exceptions_reraised(self, side_effect, expected_type):
65 def test_safe_exceptions_reraised(self, side_effect, expected_type):
66 @hg.reraise_safe_exceptions
66 @hg.reraise_safe_exceptions
67 def fake_method():
67 def fake_method():
68 raise side_effect
68 raise side_effect
69
69
70 with pytest.raises(Exception) as exc_info:
70 with pytest.raises(Exception) as exc_info:
71 fake_method()
71 fake_method()
72 assert type(exc_info.value) == Exception
72 assert type(exc_info.value) == Exception
73 assert exc_info.value._vcs_kind == expected_type
73 assert exc_info.value._vcs_kind == expected_type
74
74
75 def test_keeps_original_traceback(self):
75 def test_keeps_original_traceback(self):
76 @hg.reraise_safe_exceptions
76 @hg.reraise_safe_exceptions
77 def fake_method():
77 def fake_method():
78 try:
78 try:
79 raise hgcompat.Abort()
79 raise hgcompat.Abort('test-abort')
80 except:
80 except:
81 self.original_traceback = traceback.format_tb(
81 self.original_traceback = traceback.format_tb(sys.exc_info()[2])
82 sys.exc_info()[2])
83 raise
82 raise
84
83
85 try:
84 try:
86 fake_method()
85 fake_method()
87 except Exception:
86 except Exception:
88 new_traceback = traceback.format_tb(sys.exc_info()[2])
87 new_traceback = traceback.format_tb(sys.exc_info()[2])
89
88
90 new_traceback_tail = new_traceback[-len(self.original_traceback):]
89 new_traceback_tail = new_traceback[-len(self.original_traceback):]
91 assert new_traceback_tail == self.original_traceback
90 assert new_traceback_tail == self.original_traceback
92
91
93 def test_maps_unknow_exceptions_to_unhandled(self):
92 def test_maps_unknow_exceptions_to_unhandled(self):
94 @hg.reraise_safe_exceptions
93 @hg.reraise_safe_exceptions
95 def stub_method():
94 def stub_method():
96 raise ValueError('stub')
95 raise ValueError('stub')
97
96
98 with pytest.raises(Exception) as exc_info:
97 with pytest.raises(Exception) as exc_info:
99 stub_method()
98 stub_method()
100 assert exc_info.value._vcs_kind == 'unhandled'
99 assert exc_info.value._vcs_kind == 'unhandled'
101
100
102 def test_does_not_map_known_exceptions(self):
101 def test_does_not_map_known_exceptions(self):
103 @hg.reraise_safe_exceptions
102 @hg.reraise_safe_exceptions
104 def stub_method():
103 def stub_method():
105 raise exceptions.LookupException()('stub')
104 raise exceptions.LookupException()('stub')
106
105
107 with pytest.raises(Exception) as exc_info:
106 with pytest.raises(Exception) as exc_info:
108 stub_method()
107 stub_method()
109 assert exc_info.value._vcs_kind == 'lookup'
108 assert exc_info.value._vcs_kind == 'lookup'
@@ -1,124 +1,119 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import mock
18 import mock
19 import pytest
19 import pytest
20
20
21 from vcsserver import hgcompat, hgpatches
21 from vcsserver import hgcompat, hgpatches
22
22
23
23
24 LARGEFILES_CAPABILITY = 'largefiles=serve'
24 LARGEFILES_CAPABILITY = b'largefiles=serve'
25
25
26
26
27 def test_patch_largefiles_capabilities_applies_patch(
27 def test_patch_largefiles_capabilities_applies_patch(
28 patched_capabilities):
28 patched_capabilities):
29 lfproto = hgcompat.largefiles.proto
29 lfproto = hgcompat.largefiles.proto
30 hgpatches.patch_largefiles_capabilities()
30 hgpatches.patch_largefiles_capabilities()
31 assert lfproto._capabilities.__name__ == '_dynamic_capabilities'
31 assert lfproto._capabilities.__name__ == '_dynamic_capabilities'
32
32
33
33
34 def test_dynamic_capabilities_uses_original_function_if_not_enabled(
34 def test_dynamic_capabilities_uses_original_function_if_not_enabled(
35 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
35 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
36 orig_capabilities):
36 orig_capabilities):
37 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
37 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
38 hgcompat.largefiles.proto, stub_extensions)
38 hgcompat.largefiles.proto, stub_extensions)
39
39
40 caps = dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
40 caps = dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
41
41
42 stub_extensions.assert_called_once_with(stub_ui)
42 stub_extensions.assert_called_once_with(stub_ui)
43 assert LARGEFILES_CAPABILITY not in caps
43 assert LARGEFILES_CAPABILITY not in caps
44
44
45
45
46 def test_dynamic_capabilities_ignores_updated_capabilities(
46 def test_dynamic_capabilities_ignores_updated_capabilities(
47 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
47 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
48 orig_capabilities):
48 orig_capabilities):
49 stub_extensions.return_value = [('largefiles', mock.Mock())]
49 stub_extensions.return_value = [('largefiles', mock.Mock())]
50 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
50 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
51 hgcompat.largefiles.proto, stub_extensions)
51 hgcompat.largefiles.proto, stub_extensions)
52
52
53 # This happens when the extension is loaded for the first time, important
53 # This happens when the extension is loaded for the first time, important
54 # to ensure that an updated function is correctly picked up.
54 # to ensure that an updated function is correctly picked up.
55 hgcompat.largefiles.proto._capabilities = mock.Mock(
55 hgcompat.largefiles.proto._capabilities = mock.Mock(
56 side_effect=Exception('Must not be called'))
56 side_effect=Exception('Must not be called'))
57
57
58 dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
58 dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
59
59
60
60
61 def test_dynamic_capabilities_uses_largefiles_if_enabled(
61 def test_dynamic_capabilities_uses_largefiles_if_enabled(
62 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
62 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
63 orig_capabilities):
63 orig_capabilities):
64 stub_extensions.return_value = [('largefiles', mock.Mock())]
64 stub_extensions.return_value = [('largefiles', mock.Mock())]
65
65
66 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
66 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
67 hgcompat.largefiles.proto, stub_extensions)
67 hgcompat.largefiles.proto, stub_extensions)
68
68
69 caps = dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
69 caps = dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
70
70
71 stub_extensions.assert_called_once_with(stub_ui)
71 stub_extensions.assert_called_once_with(stub_ui)
72 assert LARGEFILES_CAPABILITY in caps
72 assert LARGEFILES_CAPABILITY in caps
73
73
74
74
75 def test_hgsubversion_import():
76 from hgsubversion import svnrepo
77 assert svnrepo
78
79
80 @pytest.fixture
75 @pytest.fixture
81 def patched_capabilities(request):
76 def patched_capabilities(request):
82 """
77 """
83 Patch in `capabilitiesorig` and restore both capability functions.
78 Patch in `capabilitiesorig` and restore both capability functions.
84 """
79 """
85 lfproto = hgcompat.largefiles.proto
80 lfproto = hgcompat.largefiles.proto
86 orig_capabilities = lfproto._capabilities
81 orig_capabilities = lfproto._capabilities
87
82
88 @request.addfinalizer
83 @request.addfinalizer
89 def restore():
84 def restore():
90 lfproto._capabilities = orig_capabilities
85 lfproto._capabilities = orig_capabilities
91
86
92
87
93 @pytest.fixture
88 @pytest.fixture
94 def stub_repo(stub_ui):
89 def stub_repo(stub_ui):
95 repo = mock.Mock()
90 repo = mock.Mock()
96 repo.ui = stub_ui
91 repo.ui = stub_ui
97 return repo
92 return repo
98
93
99
94
100 @pytest.fixture
95 @pytest.fixture
101 def stub_proto(stub_ui):
96 def stub_proto(stub_ui):
102 proto = mock.Mock()
97 proto = mock.Mock()
103 proto.ui = stub_ui
98 proto.ui = stub_ui
104 return proto
99 return proto
105
100
106
101
107 @pytest.fixture
102 @pytest.fixture
108 def orig_capabilities():
103 def orig_capabilities():
109 from mercurial.wireprotov1server import wireprotocaps
104 from mercurial.wireprotov1server import wireprotocaps
110
105
111 def _capabilities(repo, proto):
106 def _capabilities(repo, proto):
112 return wireprotocaps
107 return wireprotocaps
113 return _capabilities
108 return _capabilities
114
109
115
110
116 @pytest.fixture
111 @pytest.fixture
117 def stub_ui():
112 def stub_ui():
118 return hgcompat.ui.ui()
113 return hgcompat.ui.ui()
119
114
120
115
121 @pytest.fixture
116 @pytest.fixture
122 def stub_extensions():
117 def stub_extensions():
123 extensions = mock.Mock(return_value=tuple())
118 extensions = mock.Mock(return_value=tuple())
124 return extensions
119 return extensions
@@ -1,241 +1,245 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import contextlib
19 import io
20 import threading
18 import threading
19 import msgpack
20
21 from http.server import BaseHTTPRequestHandler
21 from http.server import BaseHTTPRequestHandler
22 from socketserver import TCPServer
22 from socketserver import TCPServer
23
23
24 import mercurial.ui
24 import mercurial.ui
25 import mock
25 import mock
26 import pytest
26 import pytest
27 import simplejson as json
28
27
28 from vcsserver.lib.rc_json import json
29 from vcsserver import hooks
29 from vcsserver import hooks
30
30
31
31
32 def get_hg_ui(extras=None):
32 def get_hg_ui(extras=None):
33 """Create a Config object with a valid RC_SCM_DATA entry."""
33 """Create a Config object with a valid RC_SCM_DATA entry."""
34 extras = extras or {}
34 extras = extras or {}
35 required_extras = {
35 required_extras = {
36 'username': '',
36 'username': '',
37 'repository': '',
37 'repository': '',
38 'locked_by': '',
38 'locked_by': '',
39 'scm': '',
39 'scm': '',
40 'make_lock': '',
40 'make_lock': '',
41 'action': '',
41 'action': '',
42 'ip': '',
42 'ip': '',
43 'hooks_uri': 'fake_hooks_uri',
43 'hooks_uri': 'fake_hooks_uri',
44 }
44 }
45 required_extras.update(extras)
45 required_extras.update(extras)
46 hg_ui = mercurial.ui.ui()
46 hg_ui = mercurial.ui.ui()
47 hg_ui.setconfig('rhodecode', 'RC_SCM_DATA', json.dumps(required_extras))
47 hg_ui.setconfig(b'rhodecode', b'RC_SCM_DATA', json.dumps(required_extras))
48
48
49 return hg_ui
49 return hg_ui
50
50
51
51
52 def test_git_pre_receive_is_disabled():
52 def test_git_pre_receive_is_disabled():
53 extras = {'hooks': ['pull']}
53 extras = {'hooks': ['pull']}
54 response = hooks.git_pre_receive(None, None,
54 response = hooks.git_pre_receive(None, None,
55 {'RC_SCM_DATA': json.dumps(extras)})
55 {'RC_SCM_DATA': json.dumps(extras)})
56
56
57 assert response == 0
57 assert response == 0
58
58
59
59
60 def test_git_post_receive_is_disabled():
60 def test_git_post_receive_is_disabled():
61 extras = {'hooks': ['pull']}
61 extras = {'hooks': ['pull']}
62 response = hooks.git_post_receive(None, '',
62 response = hooks.git_post_receive(None, '',
63 {'RC_SCM_DATA': json.dumps(extras)})
63 {'RC_SCM_DATA': json.dumps(extras)})
64
64
65 assert response == 0
65 assert response == 0
66
66
67
67
68 def test_git_post_receive_calls_repo_size():
68 def test_git_post_receive_calls_repo_size():
69 extras = {'hooks': ['push', 'repo_size']}
69 extras = {'hooks': ['push', 'repo_size']}
70
70 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
71 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
71 hooks.git_post_receive(
72 hooks.git_post_receive(
72 None, '', {'RC_SCM_DATA': json.dumps(extras)})
73 None, '', {'RC_SCM_DATA': json.dumps(extras)})
73 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
74 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
74 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
75 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
75 expected_calls = [
76 expected_calls = [
76 mock.call('repo_size', extras, mock.ANY),
77 mock.call('repo_size', extras, mock.ANY),
77 mock.call('post_push', extras, mock.ANY),
78 mock.call('post_push', extras, mock.ANY),
78 ]
79 ]
79 assert call_hook_mock.call_args_list == expected_calls
80 assert call_hook_mock.call_args_list == expected_calls
80
81
81
82
82 def test_git_post_receive_does_not_call_disabled_repo_size():
83 def test_git_post_receive_does_not_call_disabled_repo_size():
83 extras = {'hooks': ['push']}
84 extras = {'hooks': ['push']}
85
84 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
86 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
85 hooks.git_post_receive(
87 hooks.git_post_receive(
86 None, '', {'RC_SCM_DATA': json.dumps(extras)})
88 None, '', {'RC_SCM_DATA': json.dumps(extras)})
87 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
89 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
88 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
90 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
89 expected_calls = [
91 expected_calls = [
90 mock.call('post_push', extras, mock.ANY)
92 mock.call('post_push', extras, mock.ANY)
91 ]
93 ]
92 assert call_hook_mock.call_args_list == expected_calls
94 assert call_hook_mock.call_args_list == expected_calls
93
95
94
96
95 def test_repo_size_exception_does_not_affect_git_post_receive():
97 def test_repo_size_exception_does_not_affect_git_post_receive():
96 extras = {'hooks': ['push', 'repo_size']}
98 extras = {'hooks': ['push', 'repo_size']}
97 status = 0
99 status = 0
98
100
99 def side_effect(name, *args, **kwargs):
101 def side_effect(name, *args, **kwargs):
100 if name == 'repo_size':
102 if name == 'repo_size':
101 raise Exception('Fake exception')
103 raise Exception('Fake exception')
102 else:
104 else:
103 return status
105 return status
104
106
105 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
107 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
106 call_hook_mock.side_effect = side_effect
108 call_hook_mock.side_effect = side_effect
107 result = hooks.git_post_receive(
109 result = hooks.git_post_receive(
108 None, '', {'RC_SCM_DATA': json.dumps(extras)})
110 None, '', {'RC_SCM_DATA': json.dumps(extras)})
109 assert result == status
111 assert result == status
110
112
111
113
112 def test_git_pre_pull_is_disabled():
114 def test_git_pre_pull_is_disabled():
113 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
115 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
114
116
115
117
116 def test_git_post_pull_is_disabled():
118 def test_git_post_pull_is_disabled():
117 assert (
119 assert (
118 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
120 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
119
121
120
122
121 class TestGetHooksClient(object):
123 class TestGetHooksClient(object):
122
124
123 def test_returns_http_client_when_protocol_matches(self):
125 def test_returns_http_client_when_protocol_matches(self):
124 hooks_uri = 'localhost:8000'
126 hooks_uri = 'localhost:8000'
125 result = hooks._get_hooks_client({
127 result = hooks._get_hooks_client({
126 'hooks_uri': hooks_uri,
128 'hooks_uri': hooks_uri,
127 'hooks_protocol': 'http'
129 'hooks_protocol': 'http'
128 })
130 })
129 assert isinstance(result, hooks.HooksHttpClient)
131 assert isinstance(result, hooks.HooksHttpClient)
130 assert result.hooks_uri == hooks_uri
132 assert result.hooks_uri == hooks_uri
131
133
132 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
134 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
133 fake_module = mock.Mock()
135 fake_module = mock.Mock()
134 import_patcher = mock.patch.object(
136 import_patcher = mock.patch.object(
135 hooks.importlib, 'import_module', return_value=fake_module)
137 hooks.importlib, 'import_module', return_value=fake_module)
136 fake_module_name = 'fake.module'
138 fake_module_name = 'fake.module'
137 with import_patcher as import_mock:
139 with import_patcher as import_mock:
138 result = hooks._get_hooks_client(
140 result = hooks._get_hooks_client(
139 {'hooks_module': fake_module_name})
141 {'hooks_module': fake_module_name})
140
142
141 import_mock.assert_called_once_with(fake_module_name)
143 import_mock.assert_called_once_with(fake_module_name)
142 assert isinstance(result, hooks.HooksDummyClient)
144 assert isinstance(result, hooks.HooksDummyClient)
143 assert result._hooks_module == fake_module
145 assert result._hooks_module == fake_module
144
146
145
147
146 class TestHooksHttpClient(object):
148 class TestHooksHttpClient(object):
147 def test_init_sets_hooks_uri(self):
149 def test_init_sets_hooks_uri(self):
148 uri = 'localhost:3000'
150 uri = 'localhost:3000'
149 client = hooks.HooksHttpClient(uri)
151 client = hooks.HooksHttpClient(uri)
150 assert client.hooks_uri == uri
152 assert client.hooks_uri == uri
151
153
152 def test_serialize_returns_json_string(self):
154 def test_serialize_returns_serialized_string(self):
153 client = hooks.HooksHttpClient('localhost:3000')
155 client = hooks.HooksHttpClient('localhost:3000')
154 hook_name = 'test'
156 hook_name = 'test'
155 extras = {
157 extras = {
156 'first': 1,
158 'first': 1,
157 'second': 'two'
159 'second': 'two'
158 }
160 }
159 result = client._serialize(hook_name, extras)
161 hooks_proto, result = client._serialize(hook_name, extras)
160 expected_result = json.dumps({
162 expected_result = msgpack.packb({
161 'method': hook_name,
163 'method': hook_name,
162 'extras': extras
164 'extras': extras,
163 })
165 })
166 assert hooks_proto == {'rc-hooks-protocol': 'msgpack.v1'}
164 assert result == expected_result
167 assert result == expected_result
165
168
166 def test_call_queries_http_server(self, http_mirror):
169 def test_call_queries_http_server(self, http_mirror):
167 client = hooks.HooksHttpClient(http_mirror.uri)
170 client = hooks.HooksHttpClient(http_mirror.uri)
168 hook_name = 'test'
171 hook_name = 'test'
169 extras = {
172 extras = {
170 'first': 1,
173 'first': 1,
171 'second': 'two'
174 'second': 'two'
172 }
175 }
173 result = client(hook_name, extras)
176 result = client(hook_name, extras)
174 expected_result = {
177 expected_result = msgpack.unpackb(msgpack.packb({
175 'method': hook_name,
178 'method': hook_name,
176 'extras': extras
179 'extras': extras
177 }
180 }), raw=False)
178 assert result == expected_result
181 assert result == expected_result
179
182
180
183
181 class TestHooksDummyClient(object):
184 class TestHooksDummyClient(object):
182 def test_init_imports_hooks_module(self):
185 def test_init_imports_hooks_module(self):
183 hooks_module_name = 'rhodecode.fake.module'
186 hooks_module_name = 'rhodecode.fake.module'
184 hooks_module = mock.MagicMock()
187 hooks_module = mock.MagicMock()
185
188
186 import_patcher = mock.patch.object(
189 import_patcher = mock.patch.object(
187 hooks.importlib, 'import_module', return_value=hooks_module)
190 hooks.importlib, 'import_module', return_value=hooks_module)
188 with import_patcher as import_mock:
191 with import_patcher as import_mock:
189 client = hooks.HooksDummyClient(hooks_module_name)
192 client = hooks.HooksDummyClient(hooks_module_name)
190 import_mock.assert_called_once_with(hooks_module_name)
193 import_mock.assert_called_once_with(hooks_module_name)
191 assert client._hooks_module == hooks_module
194 assert client._hooks_module == hooks_module
192
195
193 def test_call_returns_hook_result(self):
196 def test_call_returns_hook_result(self):
194 hooks_module_name = 'rhodecode.fake.module'
197 hooks_module_name = 'rhodecode.fake.module'
195 hooks_module = mock.MagicMock()
198 hooks_module = mock.MagicMock()
196 import_patcher = mock.patch.object(
199 import_patcher = mock.patch.object(
197 hooks.importlib, 'import_module', return_value=hooks_module)
200 hooks.importlib, 'import_module', return_value=hooks_module)
198 with import_patcher:
201 with import_patcher:
199 client = hooks.HooksDummyClient(hooks_module_name)
202 client = hooks.HooksDummyClient(hooks_module_name)
200
203
201 result = client('post_push', {})
204 result = client('post_push', {})
202 hooks_module.Hooks.assert_called_once_with()
205 hooks_module.Hooks.assert_called_once_with()
203 assert result == hooks_module.Hooks().__enter__().post_push()
206 assert result == hooks_module.Hooks().__enter__().post_push()
204
207
205
208
206 @pytest.fixture
209 @pytest.fixture
207 def http_mirror(request):
210 def http_mirror(request):
208 server = MirrorHttpServer()
211 server = MirrorHttpServer()
209 request.addfinalizer(server.stop)
212 request.addfinalizer(server.stop)
210 return server
213 return server
211
214
212
215
213 class MirrorHttpHandler(BaseHTTPRequestHandler):
216 class MirrorHttpHandler(BaseHTTPRequestHandler):
217
214 def do_POST(self):
218 def do_POST(self):
215 length = int(self.headers['Content-Length'])
219 length = int(self.headers['Content-Length'])
216 body = self.rfile.read(length).decode('utf-8')
220 body = self.rfile.read(length)
217 self.send_response(200)
221 self.send_response(200)
218 self.end_headers()
222 self.end_headers()
219 self.wfile.write(body)
223 self.wfile.write(body)
220
224
221
225
222 class MirrorHttpServer(object):
226 class MirrorHttpServer(object):
223 ip_address = '127.0.0.1'
227 ip_address = '127.0.0.1'
224 port = 0
228 port = 0
225
229
226 def __init__(self):
230 def __init__(self):
227 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
231 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
228 _, self.port = self._daemon.server_address
232 _, self.port = self._daemon.server_address
229 self._thread = threading.Thread(target=self._daemon.serve_forever)
233 self._thread = threading.Thread(target=self._daemon.serve_forever)
230 self._thread.daemon = True
234 self._thread.daemon = True
231 self._thread.start()
235 self._thread.start()
232
236
233 def stop(self):
237 def stop(self):
234 self._daemon.shutdown()
238 self._daemon.shutdown()
235 self._thread.join()
239 self._thread.join()
236 self._daemon = None
240 self._daemon = None
237 self._thread = None
241 self._thread = None
238
242
239 @property
243 @property
240 def uri(self):
244 def uri(self):
241 return '{}:{}'.format(self.ip_address, self.port)
245 return '{}:{}'.format(self.ip_address, self.port)
@@ -1,42 +1,42 b''
1 """
1 """
2 Tests used to profile the HTTP based implementation.
2 Tests used to profile the HTTP based implementation.
3 """
3 """
4
4
5 import pytest
5 import pytest
6 import webtest
6 import webtest
7
7
8 from vcsserver.http_main import main
8 from vcsserver.http_main import main
9
9
10
10
11 @pytest.fixture
11 @pytest.fixture
12 def vcs_app():
12 def vcs_app():
13 stub_settings = {
13 stub_settings = {
14 'dev.use_echo_app': 'true',
14 'dev.use_echo_app': 'true',
15 'locale': 'en_US.UTF-8',
15 'locale': 'en_US.UTF-8',
16 }
16 }
17 stub_global_conf = {
17 stub_global_conf = {
18 '__file__': ''
18 '__file__': ''
19 }
19 }
20 vcs_app = main(stub_global_conf, **stub_settings)
20 vcs_app = main(stub_global_conf, **stub_settings)
21 app = webtest.TestApp(vcs_app)
21 app = webtest.TestApp(vcs_app)
22 return app
22 return app
23
23
24
24
25 @pytest.fixture(scope='module')
25 @pytest.fixture(scope='module')
26 def data():
26 def data():
27 one_kb = 'x' * 1024
27 one_kb = 'x' * 1024
28 return one_kb * 1024 * 10
28 return one_kb * 1024 * 10
29
29
30
30
31 def test_http_app_streaming_with_data(data, repeat, vcs_app):
31 def test_http_app_streaming_with_data(data, repeat, vcs_app):
32 app = vcs_app
32 app = vcs_app
33 for x in range(repeat / 10):
33 for x in range(repeat // 10):
34 response = app.post('/stream/git/', params=data)
34 response = app.post('/stream/git/', params=data)
35 assert response.status_code == 200
35 assert response.status_code == 200
36
36
37
37
38 def test_http_app_streaming_no_data(repeat, vcs_app):
38 def test_http_app_streaming_no_data(repeat, vcs_app):
39 app = vcs_app
39 app = vcs_app
40 for x in range(repeat / 10):
40 for x in range(repeat // 10):
41 response = app.post('/stream/git/')
41 response = app.post('/stream/git/')
42 assert response.status_code == 200
42 assert response.status_code == 200
@@ -1,206 +1,205 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import sys
19 import sys
20 import stat
20 import stat
21 import pytest
21 import pytest
22 import vcsserver
22 import vcsserver
23 import tempfile
23 import tempfile
24 from vcsserver import hook_utils
24 from vcsserver import hook_utils
25 from vcsserver.tests.fixture import no_newline_id_generator
25 from vcsserver.tests.fixture import no_newline_id_generator
26 from vcsserver.utils import AttributeDict
26 from vcsserver.utils import AttributeDict, safe_bytes, safe_str
27
27
28
28
29 class TestCheckRhodecodeHook(object):
29 class TestCheckRhodecodeHook(object):
30
30
31 def test_returns_false_when_hook_file_is_wrong_found(self, tmpdir):
31 def test_returns_false_when_hook_file_is_wrong_found(self, tmpdir):
32 hook = os.path.join(str(tmpdir), 'fake_hook_file.py')
32 hook = os.path.join(str(tmpdir), 'fake_hook_file.py')
33 with open(hook, 'wb') as f:
33 with open(hook, 'wb') as f:
34 f.write('dummy test')
34 f.write(b'dummy test')
35 result = hook_utils.check_rhodecode_hook(hook)
35 result = hook_utils.check_rhodecode_hook(hook)
36 assert result is False
36 assert result is False
37
37
38 def test_returns_true_when_no_hook_file_found(self, tmpdir):
38 def test_returns_true_when_no_hook_file_found(self, tmpdir):
39 hook = os.path.join(str(tmpdir), 'fake_hook_file_not_existing.py')
39 hook = os.path.join(str(tmpdir), 'fake_hook_file_not_existing.py')
40 result = hook_utils.check_rhodecode_hook(hook)
40 result = hook_utils.check_rhodecode_hook(hook)
41 assert result
41 assert result
42
42
43 @pytest.mark.parametrize("file_content, expected_result", [
43 @pytest.mark.parametrize("file_content, expected_result", [
44 ("RC_HOOK_VER = '3.3.3'\n", True),
44 ("RC_HOOK_VER = '3.3.3'\n", True),
45 ("RC_HOOK = '3.3.3'\n", False),
45 ("RC_HOOK = '3.3.3'\n", False),
46 ], ids=no_newline_id_generator)
46 ], ids=no_newline_id_generator)
47 def test_signatures(self, file_content, expected_result, tmpdir):
47 def test_signatures(self, file_content, expected_result, tmpdir):
48 hook = os.path.join(str(tmpdir), 'fake_hook_file_1.py')
48 hook = os.path.join(str(tmpdir), 'fake_hook_file_1.py')
49 with open(hook, 'wb') as f:
49 with open(hook, 'wb') as f:
50 f.write(file_content)
50 f.write(safe_bytes(file_content))
51
51
52 result = hook_utils.check_rhodecode_hook(hook)
52 result = hook_utils.check_rhodecode_hook(hook)
53
53
54 assert result is expected_result
54 assert result is expected_result
55
55
56
56
57 class BaseInstallHooks(object):
57 class BaseInstallHooks(object):
58 HOOK_FILES = ()
58 HOOK_FILES = ()
59
59
60 def _check_hook_file_mode(self, file_path):
60 def _check_hook_file_mode(self, file_path):
61 assert os.path.exists(file_path), 'path %s missing' % file_path
61 assert os.path.exists(file_path), 'path %s missing' % file_path
62 stat_info = os.stat(file_path)
62 stat_info = os.stat(file_path)
63
63
64 file_mode = stat.S_IMODE(stat_info.st_mode)
64 file_mode = stat.S_IMODE(stat_info.st_mode)
65 expected_mode = int('755', 8)
65 expected_mode = int('755', 8)
66 assert expected_mode == file_mode
66 assert expected_mode == file_mode
67
67
68 def _check_hook_file_content(self, file_path, executable):
68 def _check_hook_file_content(self, file_path, executable):
69 executable = executable or sys.executable
69 executable = executable or sys.executable
70 with open(file_path, 'rt') as hook_file:
70 with open(file_path, 'rt') as hook_file:
71 content = hook_file.read()
71 content = hook_file.read()
72
72
73 expected_env = '#!{}'.format(executable)
73 expected_env = '#!{}'.format(executable)
74 expected_rc_version = "\nRC_HOOK_VER = '{}'\n".format(
74 expected_rc_version = "\nRC_HOOK_VER = '{}'\n".format(safe_str(vcsserver.__version__))
75 vcsserver.__version__)
76 assert content.strip().startswith(expected_env)
75 assert content.strip().startswith(expected_env)
77 assert expected_rc_version in content
76 assert expected_rc_version in content
78
77
79 def _create_fake_hook(self, file_path, content):
78 def _create_fake_hook(self, file_path, content):
80 with open(file_path, 'w') as hook_file:
79 with open(file_path, 'w') as hook_file:
81 hook_file.write(content)
80 hook_file.write(content)
82
81
83 def create_dummy_repo(self, repo_type):
82 def create_dummy_repo(self, repo_type):
84 tmpdir = tempfile.mkdtemp()
83 tmpdir = tempfile.mkdtemp()
85 repo = AttributeDict()
84 repo = AttributeDict()
86 if repo_type == 'git':
85 if repo_type == 'git':
87 repo.path = os.path.join(tmpdir, 'test_git_hooks_installation_repo')
86 repo.path = os.path.join(tmpdir, 'test_git_hooks_installation_repo')
88 os.makedirs(repo.path)
87 os.makedirs(repo.path)
89 os.makedirs(os.path.join(repo.path, 'hooks'))
88 os.makedirs(os.path.join(repo.path, 'hooks'))
90 repo.bare = True
89 repo.bare = True
91
90
92 elif repo_type == 'svn':
91 elif repo_type == 'svn':
93 repo.path = os.path.join(tmpdir, 'test_svn_hooks_installation_repo')
92 repo.path = os.path.join(tmpdir, 'test_svn_hooks_installation_repo')
94 os.makedirs(repo.path)
93 os.makedirs(repo.path)
95 os.makedirs(os.path.join(repo.path, 'hooks'))
94 os.makedirs(os.path.join(repo.path, 'hooks'))
96
95
97 return repo
96 return repo
98
97
99 def check_hooks(self, repo_path, repo_bare=True):
98 def check_hooks(self, repo_path, repo_bare=True):
100 for file_name in self.HOOK_FILES:
99 for file_name in self.HOOK_FILES:
101 if repo_bare:
100 if repo_bare:
102 file_path = os.path.join(repo_path, 'hooks', file_name)
101 file_path = os.path.join(repo_path, 'hooks', file_name)
103 else:
102 else:
104 file_path = os.path.join(repo_path, '.git', 'hooks', file_name)
103 file_path = os.path.join(repo_path, '.git', 'hooks', file_name)
105 self._check_hook_file_mode(file_path)
104 self._check_hook_file_mode(file_path)
106 self._check_hook_file_content(file_path, sys.executable)
105 self._check_hook_file_content(file_path, sys.executable)
107
106
108
107
109 class TestInstallGitHooks(BaseInstallHooks):
108 class TestInstallGitHooks(BaseInstallHooks):
110 HOOK_FILES = ('pre-receive', 'post-receive')
109 HOOK_FILES = ('pre-receive', 'post-receive')
111
110
112 def test_hooks_are_installed(self):
111 def test_hooks_are_installed(self):
113 repo = self.create_dummy_repo('git')
112 repo = self.create_dummy_repo('git')
114 result = hook_utils.install_git_hooks(repo.path, repo.bare)
113 result = hook_utils.install_git_hooks(repo.path, repo.bare)
115 assert result
114 assert result
116 self.check_hooks(repo.path, repo.bare)
115 self.check_hooks(repo.path, repo.bare)
117
116
118 def test_hooks_are_replaced(self):
117 def test_hooks_are_replaced(self):
119 repo = self.create_dummy_repo('git')
118 repo = self.create_dummy_repo('git')
120 hooks_path = os.path.join(repo.path, 'hooks')
119 hooks_path = os.path.join(repo.path, 'hooks')
121 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
120 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
122 self._create_fake_hook(
121 self._create_fake_hook(
123 file_path, content="RC_HOOK_VER = 'abcde'\n")
122 file_path, content="RC_HOOK_VER = 'abcde'\n")
124
123
125 result = hook_utils.install_git_hooks(repo.path, repo.bare)
124 result = hook_utils.install_git_hooks(repo.path, repo.bare)
126 assert result
125 assert result
127 self.check_hooks(repo.path, repo.bare)
126 self.check_hooks(repo.path, repo.bare)
128
127
129 def test_non_rc_hooks_are_not_replaced(self):
128 def test_non_rc_hooks_are_not_replaced(self):
130 repo = self.create_dummy_repo('git')
129 repo = self.create_dummy_repo('git')
131 hooks_path = os.path.join(repo.path, 'hooks')
130 hooks_path = os.path.join(repo.path, 'hooks')
132 non_rc_content = 'echo "non rc hook"\n'
131 non_rc_content = 'echo "non rc hook"\n'
133 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
132 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
134 self._create_fake_hook(
133 self._create_fake_hook(
135 file_path, content=non_rc_content)
134 file_path, content=non_rc_content)
136
135
137 result = hook_utils.install_git_hooks(repo.path, repo.bare)
136 result = hook_utils.install_git_hooks(repo.path, repo.bare)
138 assert result
137 assert result
139
138
140 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
139 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
141 with open(file_path, 'rt') as hook_file:
140 with open(file_path, 'rt') as hook_file:
142 content = hook_file.read()
141 content = hook_file.read()
143 assert content == non_rc_content
142 assert content == non_rc_content
144
143
145 def test_non_rc_hooks_are_replaced_with_force_flag(self):
144 def test_non_rc_hooks_are_replaced_with_force_flag(self):
146 repo = self.create_dummy_repo('git')
145 repo = self.create_dummy_repo('git')
147 hooks_path = os.path.join(repo.path, 'hooks')
146 hooks_path = os.path.join(repo.path, 'hooks')
148 non_rc_content = 'echo "non rc hook"\n'
147 non_rc_content = 'echo "non rc hook"\n'
149 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
148 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
150 self._create_fake_hook(
149 self._create_fake_hook(
151 file_path, content=non_rc_content)
150 file_path, content=non_rc_content)
152
151
153 result = hook_utils.install_git_hooks(
152 result = hook_utils.install_git_hooks(
154 repo.path, repo.bare, force_create=True)
153 repo.path, repo.bare, force_create=True)
155 assert result
154 assert result
156 self.check_hooks(repo.path, repo.bare)
155 self.check_hooks(repo.path, repo.bare)
157
156
158
157
159 class TestInstallSvnHooks(BaseInstallHooks):
158 class TestInstallSvnHooks(BaseInstallHooks):
160 HOOK_FILES = ('pre-commit', 'post-commit')
159 HOOK_FILES = ('pre-commit', 'post-commit')
161
160
162 def test_hooks_are_installed(self):
161 def test_hooks_are_installed(self):
163 repo = self.create_dummy_repo('svn')
162 repo = self.create_dummy_repo('svn')
164 result = hook_utils.install_svn_hooks(repo.path)
163 result = hook_utils.install_svn_hooks(repo.path)
165 assert result
164 assert result
166 self.check_hooks(repo.path)
165 self.check_hooks(repo.path)
167
166
168 def test_hooks_are_replaced(self):
167 def test_hooks_are_replaced(self):
169 repo = self.create_dummy_repo('svn')
168 repo = self.create_dummy_repo('svn')
170 hooks_path = os.path.join(repo.path, 'hooks')
169 hooks_path = os.path.join(repo.path, 'hooks')
171 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
170 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
172 self._create_fake_hook(
171 self._create_fake_hook(
173 file_path, content="RC_HOOK_VER = 'abcde'\n")
172 file_path, content="RC_HOOK_VER = 'abcde'\n")
174
173
175 result = hook_utils.install_svn_hooks(repo.path)
174 result = hook_utils.install_svn_hooks(repo.path)
176 assert result
175 assert result
177 self.check_hooks(repo.path)
176 self.check_hooks(repo.path)
178
177
179 def test_non_rc_hooks_are_not_replaced(self):
178 def test_non_rc_hooks_are_not_replaced(self):
180 repo = self.create_dummy_repo('svn')
179 repo = self.create_dummy_repo('svn')
181 hooks_path = os.path.join(repo.path, 'hooks')
180 hooks_path = os.path.join(repo.path, 'hooks')
182 non_rc_content = 'echo "non rc hook"\n'
181 non_rc_content = 'echo "non rc hook"\n'
183 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
182 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
184 self._create_fake_hook(
183 self._create_fake_hook(
185 file_path, content=non_rc_content)
184 file_path, content=non_rc_content)
186
185
187 result = hook_utils.install_svn_hooks(repo.path)
186 result = hook_utils.install_svn_hooks(repo.path)
188 assert result
187 assert result
189
188
190 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
189 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
191 with open(file_path, 'rt') as hook_file:
190 with open(file_path, 'rt') as hook_file:
192 content = hook_file.read()
191 content = hook_file.read()
193 assert content == non_rc_content
192 assert content == non_rc_content
194
193
195 def test_non_rc_hooks_are_replaced_with_force_flag(self):
194 def test_non_rc_hooks_are_replaced_with_force_flag(self):
196 repo = self.create_dummy_repo('svn')
195 repo = self.create_dummy_repo('svn')
197 hooks_path = os.path.join(repo.path, 'hooks')
196 hooks_path = os.path.join(repo.path, 'hooks')
198 non_rc_content = 'echo "non rc hook"\n'
197 non_rc_content = 'echo "non rc hook"\n'
199 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
198 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
200 self._create_fake_hook(
199 self._create_fake_hook(
201 file_path, content=non_rc_content)
200 file_path, content=non_rc_content)
202
201
203 result = hook_utils.install_svn_hooks(
202 result = hook_utils.install_svn_hooks(
204 repo.path, force_create=True)
203 repo.path, force_create=True)
205 assert result
204 assert result
206 self.check_hooks(repo.path, )
205 self.check_hooks(repo.path, )
@@ -1,57 +1,56 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import mock
18 import mock
19 import pytest
19 import pytest
20
20
21 from vcsserver import http_main
21 from vcsserver import http_main
22 from vcsserver.base import obfuscate_qs
22 from vcsserver.base import obfuscate_qs
23
23
24
24
25 @mock.patch('vcsserver.http_main.VCS', mock.Mock())
25 @mock.patch('vcsserver.http_main.VCS', mock.Mock())
26 @mock.patch('vcsserver.hgpatches.patch_largefiles_capabilities')
26 @mock.patch('vcsserver.hgpatches.patch_largefiles_capabilities')
27 def test_applies_largefiles_patch(patch_largefiles_capabilities):
27 def test_applies_largefiles_patch(patch_largefiles_capabilities):
28 http_main.main({'__file__': ''})
28 http_main.main({'__file__': ''})
29 patch_largefiles_capabilities.assert_called_once_with()
29 patch_largefiles_capabilities.assert_called_once_with()
30
30
31
31
32 @mock.patch('vcsserver.http_main.VCS', mock.Mock())
32 @mock.patch('vcsserver.http_main.VCS', mock.Mock())
33 @mock.patch('vcsserver.http_main.MercurialFactory', None)
33 @mock.patch('vcsserver.http_main.MercurialFactory', None)
34 @mock.patch(
34 @mock.patch(
35 'vcsserver.hgpatches.patch_largefiles_capabilities',
35 'vcsserver.hgpatches.patch_largefiles_capabilities',
36 mock.Mock(side_effect=Exception("Must not be called")))
36 mock.Mock(side_effect=Exception("Must not be called")))
37 def test_applies_largefiles_patch_only_if_mercurial_is_available():
37 def test_applies_largefiles_patch_only_if_mercurial_is_available():
38 http_main.main({'__file__': ''})
38 http_main.main({'__file__': ''})
39
39
40
40
41 @pytest.mark.parametrize('given, expected', [
41 @pytest.mark.parametrize('given, expected', [
42 ('bad', 'bad'),
42 ('bad', 'bad'),
43 ('query&foo=bar', 'query&foo=bar'),
43 ('query&foo=bar', 'query&foo=bar'),
44 ('equery&auth_token=bar', 'equery&auth_token=*****'),
44 ('equery&auth_token=bar', 'equery&auth_token=*****'),
45 ('a;b;c;query&foo=bar&auth_token=secret',
45 ('a;b;c;query&foo=bar&auth_token=secret', 'a;b;c;query&foo=bar&auth_token=*****'),
46 'a&b&c&query&foo=bar&auth_token=*****'),
47 ('', ''),
46 ('', ''),
48 (None, None),
47 (None, None),
49 ('foo=bar', 'foo=bar'),
48 ('foo=bar', 'foo=bar'),
50 ('auth_token=secret', 'auth_token=*****'),
49 ('auth_token=secret', 'auth_token=*****'),
51 ('auth_token=secret&api_key=secret2',
50 ('auth_token=secret&api_key=secret2',
52 'auth_token=*****&api_key=*****'),
51 'auth_token=*****&api_key=*****'),
53 ('auth_token=secret&api_key=secret2&param=value',
52 ('auth_token=secret&api_key=secret2&param=value',
54 'auth_token=*****&api_key=*****&param=value'),
53 'auth_token=*****&api_key=*****&param=value'),
55 ])
54 ])
56 def test_obfuscate_qs(given, expected):
55 def test_obfuscate_qs(given, expected):
57 assert expected == obfuscate_qs(given)
56 assert expected == obfuscate_qs(given)
@@ -1,249 +1,288 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import io
18 import io
19 import more_itertools
19
20
20 import dulwich.protocol
21 import dulwich.protocol
21 import mock
22 import mock
22 import pytest
23 import pytest
23 import webob
24 import webob
24 import webtest
25 import webtest
25
26
26 from vcsserver import hooks, pygrack
27 from vcsserver import hooks, pygrack
27
28
28 # pylint: disable=redefined-outer-name,protected-access
29 # pylint: disable=redefined-outer-name,protected-access
30 from vcsserver.utils import ascii_bytes
29
31
30
32
31 @pytest.fixture()
33 @pytest.fixture()
32 def pygrack_instance(tmpdir):
34 def pygrack_instance(tmpdir):
33 """
35 """
34 Creates a pygrack app instance.
36 Creates a pygrack app instance.
35
37
36 Right now, it does not much helpful regarding the passed directory.
38 Right now, it does not much helpful regarding the passed directory.
37 It just contains the required folders to pass the signature test.
39 It just contains the required folders to pass the signature test.
38 """
40 """
39 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
41 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
40 tmpdir.mkdir(dir_name)
42 tmpdir.mkdir(dir_name)
41
43
42 return pygrack.GitRepository('repo_name', str(tmpdir), 'git', False, {})
44 return pygrack.GitRepository('repo_name', str(tmpdir), 'git', False, {})
43
45
44
46
45 @pytest.fixture()
47 @pytest.fixture()
46 def pygrack_app(pygrack_instance):
48 def pygrack_app(pygrack_instance):
47 """
49 """
48 Creates a pygrack app wrapped in webtest.TestApp.
50 Creates a pygrack app wrapped in webtest.TestApp.
49 """
51 """
50 return webtest.TestApp(pygrack_instance)
52 return webtest.TestApp(pygrack_instance)
51
53
52
54
53 def test_invalid_service_info_refs_returns_403(pygrack_app):
55 def test_invalid_service_info_refs_returns_403(pygrack_app):
54 response = pygrack_app.get('/info/refs?service=git-upload-packs',
56 response = pygrack_app.get('/info/refs?service=git-upload-packs',
55 expect_errors=True)
57 expect_errors=True)
56
58
57 assert response.status_int == 403
59 assert response.status_int == 403
58
60
59
61
60 def test_invalid_endpoint_returns_403(pygrack_app):
62 def test_invalid_endpoint_returns_403(pygrack_app):
61 response = pygrack_app.post('/git-upload-packs', expect_errors=True)
63 response = pygrack_app.post('/git-upload-packs', expect_errors=True)
62
64
63 assert response.status_int == 403
65 assert response.status_int == 403
64
66
65
67
66 @pytest.mark.parametrize('sideband', [
68 @pytest.mark.parametrize('sideband', [
67 'side-band-64k',
69 'side-band-64k',
68 'side-band',
70 'side-band',
69 'side-band no-progress',
71 'side-band no-progress',
70 ])
72 ])
71 def test_pre_pull_hook_fails_with_sideband(pygrack_app, sideband):
73 def test_pre_pull_hook_fails_with_sideband(pygrack_app, sideband):
72 request = ''.join([
74 request = ''.join([
73 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ',
75 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ',
74 'multi_ack %s ofs-delta\n' % sideband,
76 'multi_ack %s ofs-delta\n' % sideband,
75 '0000',
77 '0000',
76 '0009done\n',
78 '0009done\n',
77 ])
79 ])
78 with mock.patch('vcsserver.hooks.git_pre_pull',
80 with mock.patch('vcsserver.hooks.git_pre_pull', return_value=hooks.HookResponse(1, 'foo')):
79 return_value=hooks.HookResponse(1, 'foo')):
80 response = pygrack_app.post(
81 response = pygrack_app.post(
81 '/git-upload-pack', params=request,
82 '/git-upload-pack', params=request,
82 content_type='application/x-git-upload-pack')
83 content_type='application/x-git-upload-pack')
83
84
84 data = io.BytesIO(response.body)
85 data = io.BytesIO(response.body)
85 proto = dulwich.protocol.Protocol(data.read, None)
86 proto = dulwich.protocol.Protocol(data.read, None)
86 packets = list(proto.read_pkt_seq())
87 packets = list(proto.read_pkt_seq())
87
88
88 expected_packets = [
89 expected_packets = [
89 'NAK\n', '\x02foo', '\x02Pre pull hook failed: aborting\n',
90 b'NAK\n', b'\x02foo', b'\x02Pre pull hook failed: aborting\n',
90 '\x01' + pygrack.GitRepository.EMPTY_PACK,
91 b'\x01' + pygrack.GitRepository.EMPTY_PACK,
91 ]
92 ]
92 assert packets == expected_packets
93 assert packets == expected_packets
93
94
94
95
95 def test_pre_pull_hook_fails_no_sideband(pygrack_app):
96 def test_pre_pull_hook_fails_no_sideband(pygrack_app):
96 request = ''.join([
97 request = ''.join([
97 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
98 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
98 'multi_ack ofs-delta\n'
99 'multi_ack ofs-delta\n'
99 '0000',
100 '0000',
100 '0009done\n',
101 '0009done\n',
101 ])
102 ])
102 with mock.patch('vcsserver.hooks.git_pre_pull',
103 with mock.patch('vcsserver.hooks.git_pre_pull',
103 return_value=hooks.HookResponse(1, 'foo')):
104 return_value=hooks.HookResponse(1, 'foo')):
104 response = pygrack_app.post(
105 response = pygrack_app.post(
105 '/git-upload-pack', params=request,
106 '/git-upload-pack', params=request,
106 content_type='application/x-git-upload-pack')
107 content_type='application/x-git-upload-pack')
107
108
108 assert response.body == pygrack.GitRepository.EMPTY_PACK
109 assert response.body == pygrack.GitRepository.EMPTY_PACK
109
110
110
111
111 def test_pull_has_hook_messages(pygrack_app):
112 def test_pull_has_hook_messages(pygrack_app):
112 request = ''.join([
113 request = ''.join([
113 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
114 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
114 'multi_ack side-band-64k ofs-delta\n'
115 'multi_ack side-band-64k ofs-delta\n'
115 '0000',
116 '0000',
116 '0009done\n',
117 '0009done\n',
117 ])
118 ])
118 with mock.patch('vcsserver.hooks.git_pre_pull',
119 with mock.patch('vcsserver.hooks.git_pre_pull',
119 return_value=hooks.HookResponse(0, 'foo')):
120 return_value=hooks.HookResponse(0, 'foo')):
120 with mock.patch('vcsserver.hooks.git_post_pull',
121 with mock.patch('vcsserver.hooks.git_post_pull',
121 return_value=hooks.HookResponse(1, 'bar')):
122 return_value=hooks.HookResponse(1, 'bar')):
122 with mock.patch('vcsserver.subprocessio.SubprocessIOChunker',
123 with mock.patch('vcsserver.subprocessio.SubprocessIOChunker',
123 return_value=['0008NAK\n0009subp\n0000']):
124 return_value=more_itertools.always_iterable([b'0008NAK\n0009subp\n0000'])):
124 response = pygrack_app.post(
125 response = pygrack_app.post(
125 '/git-upload-pack', params=request,
126 '/git-upload-pack', params=request,
126 content_type='application/x-git-upload-pack')
127 content_type='application/x-git-upload-pack')
127
128
128 data = io.BytesIO(response.body)
129 data = io.BytesIO(response.body)
129 proto = dulwich.protocol.Protocol(data.read, None)
130 proto = dulwich.protocol.Protocol(data.read, None)
130 packets = list(proto.read_pkt_seq())
131 packets = list(proto.read_pkt_seq())
131
132
132 assert packets == ['NAK\n', '\x02foo', 'subp\n', '\x02bar']
133 assert packets == [b'NAK\n', b'\x02foo', b'subp\n', b'\x02bar']
133
134
134
135
135 def test_get_want_capabilities(pygrack_instance):
136 def test_get_want_capabilities(pygrack_instance):
136 data = io.BytesIO(
137 data = io.BytesIO(
137 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
138 b'0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
138 'multi_ack side-band-64k ofs-delta\n00000009done\n')
139 b'multi_ack side-band-64k ofs-delta\n00000009done\n')
139
140
140 request = webob.Request({
141 request = webob.Request({
141 'wsgi.input': data,
142 'wsgi.input': data,
142 'REQUEST_METHOD': 'POST',
143 'REQUEST_METHOD': 'POST',
143 'webob.is_body_seekable': True
144 'webob.is_body_seekable': True
144 })
145 })
145
146
146 capabilities = pygrack_instance._get_want_capabilities(request)
147 capabilities = pygrack_instance._get_want_capabilities(request)
147
148
148 assert capabilities == frozenset(
149 assert capabilities == frozenset(
149 ('ofs-delta', 'multi_ack', 'side-band-64k'))
150 (b'ofs-delta', b'multi_ack', b'side-band-64k'))
150 assert data.tell() == 0
151 assert data.tell() == 0
151
152
152
153
153 @pytest.mark.parametrize('data,capabilities,expected', [
154 @pytest.mark.parametrize('data,capabilities,expected', [
154 ('foo', [], []),
155 ('foo', [], []),
155 ('', ['side-band-64k'], []),
156 ('', [pygrack.CAPABILITY_SIDE_BAND_64K], []),
156 ('', ['side-band'], []),
157 ('', [pygrack.CAPABILITY_SIDE_BAND], []),
157 ('foo', ['side-band-64k'], ['0008\x02foo']),
158 ('foo', [pygrack.CAPABILITY_SIDE_BAND_64K], [b'0008\x02foo']),
158 ('foo', ['side-band'], ['0008\x02foo']),
159 ('foo', [pygrack.CAPABILITY_SIDE_BAND], [b'0008\x02foo']),
159 ('f'*1000, ['side-band-64k'], ['03ed\x02' + 'f' * 1000]),
160 ('f'*1000, [pygrack.CAPABILITY_SIDE_BAND_64K], [b'03ed\x02' + b'f' * 1000]),
160 ('f'*1000, ['side-band'], ['03e8\x02' + 'f' * 995, '000a\x02fffff']),
161 ('f'*1000, [pygrack.CAPABILITY_SIDE_BAND], [b'03e8\x02' + b'f' * 995, b'000a\x02fffff']),
161 ('f'*65520, ['side-band-64k'], ['fff0\x02' + 'f' * 65515, '000a\x02fffff']),
162 ('f'*65520, [pygrack.CAPABILITY_SIDE_BAND_64K], [b'fff0\x02' + b'f' * 65515, b'000a\x02fffff']),
162 ('f'*65520, ['side-band'], ['03e8\x02' + 'f' * 995] * 65 + ['0352\x02' + 'f' * 845]),
163 ('f'*65520, [pygrack.CAPABILITY_SIDE_BAND], [b'03e8\x02' + b'f' * 995] * 65 + [b'0352\x02' + b'f' * 845]),
163 ], ids=[
164 ], ids=[
164 'foo-empty',
165 'foo-empty',
165 'empty-64k', 'empty',
166 'empty-64k', 'empty',
166 'foo-64k', 'foo',
167 'foo-64k', 'foo',
167 'f-1000-64k', 'f-1000',
168 'f-1000-64k', 'f-1000',
168 'f-65520-64k', 'f-65520'])
169 'f-65520-64k', 'f-65520'])
169 def test_get_messages(pygrack_instance, data, capabilities, expected):
170 def test_get_messages(pygrack_instance, data, capabilities, expected):
170 messages = pygrack_instance._get_messages(data, capabilities)
171 messages = pygrack_instance._get_messages(data, capabilities)
171
172
172 assert messages == expected
173 assert messages == expected
173
174
174
175
175 @pytest.mark.parametrize('response,capabilities,pre_pull_messages,post_pull_messages', [
176 @pytest.mark.parametrize('response,capabilities,pre_pull_messages,post_pull_messages', [
176 # Unexpected response
177 # Unexpected response
177 ('unexpected_response', ['side-band-64k'], 'foo', 'bar'),
178 ([b'unexpected_response[no_initial_header]'], [pygrack.CAPABILITY_SIDE_BAND_64K], 'foo', 'bar'),
178 # No sideband
179 # No sideband
179 ('no-sideband', [], 'foo', 'bar'),
180 ([b'no-sideband'], [], 'foo', 'bar'),
180 # No messages
181 # No messages
181 ('no-messages', ['side-band-64k'], '', ''),
182 ([b'no-messages'], [pygrack.CAPABILITY_SIDE_BAND_64K], '', ''),
182 ])
183 ])
183 def test_inject_messages_to_response_nothing_to_do(
184 def test_inject_messages_to_response_nothing_to_do(
184 pygrack_instance, response, capabilities, pre_pull_messages,
185 pygrack_instance, response, capabilities, pre_pull_messages, post_pull_messages):
185 post_pull_messages):
186 new_response = pygrack_instance._inject_messages_to_response(
187 response, capabilities, pre_pull_messages, post_pull_messages)
188
186
189 assert new_response == response
187 new_response = pygrack_instance._build_post_pull_response(
188 more_itertools.always_iterable(response), capabilities, pre_pull_messages, post_pull_messages)
189
190 assert list(new_response) == response
190
191
191
192
192 @pytest.mark.parametrize('capabilities', [
193 @pytest.mark.parametrize('capabilities', [
193 ['side-band'],
194 [pygrack.CAPABILITY_SIDE_BAND],
194 ['side-band-64k'],
195 [pygrack.CAPABILITY_SIDE_BAND_64K],
195 ])
196 ])
196 def test_inject_messages_to_response_single_element(pygrack_instance,
197 def test_inject_messages_to_response_single_element(pygrack_instance, capabilities):
197 capabilities):
198 response = [b'0008NAK\n0009subp\n0000']
198 response = ['0008NAK\n0009subp\n0000']
199 new_response = pygrack_instance._build_post_pull_response(
199 new_response = pygrack_instance._inject_messages_to_response(
200 more_itertools.always_iterable(response), capabilities, 'foo', 'bar')
200 response, capabilities, 'foo', 'bar')
201
201
202 expected_response = [
202 expected_response = b''.join([
203 '0008NAK\n', '0008\x02foo', '0009subp\n', '0008\x02bar', '0000']
203 b'0008NAK\n',
204 b'0008\x02foo',
205 b'0009subp\n',
206 b'0008\x02bar',
207 b'0000'])
204
208
205 assert new_response == expected_response
209 assert b''.join(new_response) == expected_response
206
210
207
211
208 @pytest.mark.parametrize('capabilities', [
212 @pytest.mark.parametrize('capabilities', [
209 ['side-band'],
213 [pygrack.CAPABILITY_SIDE_BAND],
210 ['side-band-64k'],
214 [pygrack.CAPABILITY_SIDE_BAND_64K],
211 ])
215 ])
212 def test_inject_messages_to_response_multi_element(pygrack_instance,
216 def test_inject_messages_to_response_multi_element(pygrack_instance, capabilities):
213 capabilities):
217 response = more_itertools.always_iterable([
214 response = [
218 b'0008NAK\n000asubp1\n', b'000asubp2\n', b'000asubp3\n', b'000asubp4\n0000'
215 '0008NAK\n000asubp1\n', '000asubp2\n', '000asubp3\n', '000asubp4\n0000']
219 ])
216 new_response = pygrack_instance._inject_messages_to_response(
220 new_response = pygrack_instance._build_post_pull_response(response, capabilities, 'foo', 'bar')
217 response, capabilities, 'foo', 'bar')
218
221
219 expected_response = [
222 expected_response = b''.join([
220 '0008NAK\n', '0008\x02foo', '000asubp1\n', '000asubp2\n', '000asubp3\n',
223 b'0008NAK\n',
221 '000asubp4\n', '0008\x02bar', '0000'
224 b'0008\x02foo',
222 ]
225 b'000asubp1\n', b'000asubp2\n', b'000asubp3\n', b'000asubp4\n',
226 b'0008\x02bar',
227 b'0000'
228 ])
223
229
224 assert new_response == expected_response
230 assert b''.join(new_response) == expected_response
225
231
226
232
227 def test_build_failed_pre_pull_response_no_sideband(pygrack_instance):
233 def test_build_failed_pre_pull_response_no_sideband(pygrack_instance):
228 response = pygrack_instance._build_failed_pre_pull_response([], 'foo')
234 response = pygrack_instance._build_failed_pre_pull_response([], 'foo')
229
235
230 assert response == [pygrack.GitRepository.EMPTY_PACK]
236 assert response == [pygrack.GitRepository.EMPTY_PACK]
231
237
232
238
233 @pytest.mark.parametrize('capabilities', [
239 @pytest.mark.parametrize('capabilities', [
234 ['side-band'],
240 [pygrack.CAPABILITY_SIDE_BAND],
235 ['side-band-64k'],
241 [pygrack.CAPABILITY_SIDE_BAND_64K],
236 ['side-band-64k', 'no-progress'],
242 [pygrack.CAPABILITY_SIDE_BAND_64K, b'no-progress'],
237 ])
243 ])
238 def test_build_failed_pre_pull_response(pygrack_instance, capabilities):
244 def test_build_failed_pre_pull_response(pygrack_instance, capabilities):
239 response = pygrack_instance._build_failed_pre_pull_response(
245 response = pygrack_instance._build_failed_pre_pull_response(capabilities, 'foo')
240 capabilities, 'foo')
241
246
242 expected_response = [
247 expected_response = [
243 '0008NAK\n', '0008\x02foo', '0024\x02Pre pull hook failed: aborting\n',
248 b'0008NAK\n', b'0008\x02foo', b'0024\x02Pre pull hook failed: aborting\n',
244 '%04x\x01%s' % (len(pygrack.GitRepository.EMPTY_PACK) + 5,
249 b'%04x\x01%s' % (len(pygrack.GitRepository.EMPTY_PACK) + 5, pygrack.GitRepository.EMPTY_PACK),
245 pygrack.GitRepository.EMPTY_PACK),
250 pygrack.GitRepository.FLUSH_PACKET,
246 '0000',
247 ]
251 ]
248
252
249 assert response == expected_response
253 assert response == expected_response
254
255
256 def test_inject_messages_to_response_generator(pygrack_instance):
257
258 def response_generator():
259 response = [
260 # protocol start
261 b'0008NAK\n',
262 ]
263 response += [ascii_bytes(f'000asubp{x}\n') for x in range(1000)]
264 response += [
265 # protocol end
266 pygrack.GitRepository.FLUSH_PACKET
267 ]
268 for elem in response:
269 yield elem
270
271 new_response = pygrack_instance._build_post_pull_response(
272 response_generator(), [pygrack.CAPABILITY_SIDE_BAND_64K, b'no-progress'], 'PRE_PULL_MSG\n', 'POST_PULL_MSG\n')
273
274 assert iter(new_response)
275
276 expected_response = b''.join([
277 # start
278 b'0008NAK\n0012\x02PRE_PULL_MSG\n',
279 ] + [
280 # ... rest
281 ascii_bytes(f'000asubp{x}\n') for x in range(1000)
282 ] + [
283 # final message,
284 b'0013\x02POST_PULL_MSG\n0000',
285
286 ])
287
288 assert b''.join(new_response) == expected_response
@@ -1,86 +1,87 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19
19
20 import mercurial.hg
20 import mercurial.hg
21 import mercurial.ui
21 import mercurial.ui
22 import mercurial.error
22 import mercurial.error
23 import mock
23 import mock
24 import pytest
24 import pytest
25 import webtest
25 import webtest
26
26
27 from vcsserver import scm_app
27 from vcsserver import scm_app
28 from vcsserver.utils import ascii_bytes
28
29
29
30
30 def test_hg_does_not_accept_invalid_cmd(tmpdir):
31 def test_hg_does_not_accept_invalid_cmd(tmpdir):
31 repo = mercurial.hg.repository(mercurial.ui.ui(), str(tmpdir), create=True)
32 repo = mercurial.hg.repository(mercurial.ui.ui(), ascii_bytes(str(tmpdir)), create=True)
32 app = webtest.TestApp(scm_app.HgWeb(repo))
33 app = webtest.TestApp(scm_app.HgWeb(repo))
33
34
34 response = app.get('/repo?cmd=invalidcmd', expect_errors=True)
35 response = app.get('/repo?cmd=invalidcmd', expect_errors=True)
35
36
36 assert response.status_int == 400
37 assert response.status_int == 400
37
38
38
39
39 def test_create_hg_wsgi_app_requirement_error(tmpdir):
40 def test_create_hg_wsgi_app_requirement_error(tmpdir):
40 repo = mercurial.hg.repository(mercurial.ui.ui(), str(tmpdir), create=True)
41 repo = mercurial.hg.repository(mercurial.ui.ui(), ascii_bytes(str(tmpdir)), create=True)
41 config = (
42 config = (
42 ('paths', 'default', ''),
43 ('paths', 'default', ''),
43 )
44 )
44 with mock.patch('vcsserver.scm_app.HgWeb') as hgweb_mock:
45 with mock.patch('vcsserver.scm_app.HgWeb') as hgweb_mock:
45 hgweb_mock.side_effect = mercurial.error.RequirementError()
46 hgweb_mock.side_effect = mercurial.error.RequirementError()
46 with pytest.raises(Exception):
47 with pytest.raises(Exception):
47 scm_app.create_hg_wsgi_app(str(tmpdir), repo, config)
48 scm_app.create_hg_wsgi_app(str(tmpdir), repo, config)
48
49
49
50
50 def test_git_returns_not_found(tmpdir):
51 def test_git_returns_not_found(tmpdir):
51 app = webtest.TestApp(
52 app = webtest.TestApp(
52 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
53 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
53
54
54 response = app.get('/repo_name/inforefs?service=git-upload-pack',
55 response = app.get('/repo_name/inforefs?service=git-upload-pack',
55 expect_errors=True)
56 expect_errors=True)
56
57
57 assert response.status_int == 404
58 assert response.status_int == 404
58
59
59
60
60 def test_git(tmpdir):
61 def test_git(tmpdir):
61 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
62 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
62 tmpdir.mkdir(dir_name)
63 tmpdir.mkdir(dir_name)
63
64
64 app = webtest.TestApp(
65 app = webtest.TestApp(
65 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
66 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
66
67
67 # We set service to git-upload-packs to trigger a 403
68 # We set service to git-upload-packs to trigger a 403
68 response = app.get('/repo_name/inforefs?service=git-upload-packs',
69 response = app.get('/repo_name/inforefs?service=git-upload-packs',
69 expect_errors=True)
70 expect_errors=True)
70
71
71 assert response.status_int == 403
72 assert response.status_int == 403
72
73
73
74
74 def test_git_fallbacks_to_git_folder(tmpdir):
75 def test_git_fallbacks_to_git_folder(tmpdir):
75 tmpdir.mkdir('.git')
76 tmpdir.mkdir('.git')
76 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
77 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
77 tmpdir.mkdir(os.path.join('.git', dir_name))
78 tmpdir.mkdir(os.path.join('.git', dir_name))
78
79
79 app = webtest.TestApp(
80 app = webtest.TestApp(
80 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
81 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
81
82
82 # We set service to git-upload-packs to trigger a 403
83 # We set service to git-upload-packs to trigger a 403
83 response = app.get('/repo_name/inforefs?service=git-upload-packs',
84 response = app.get('/repo_name/inforefs?service=git-upload-packs',
84 expect_errors=True)
85 expect_errors=True)
85
86
86 assert response.status_int == 403
87 assert response.status_int == 403
@@ -1,155 +1,155 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import io
18 import io
19 import os
19 import os
20 import sys
20 import sys
21
21
22 import pytest
22 import pytest
23
23
24 from vcsserver import subprocessio
24 from vcsserver import subprocessio
25 from vcsserver.utils import ascii_bytes
25
26
26
27
27 class KindaFilelike(object): # pragma: no cover
28 class FileLikeObj(object): # pragma: no cover
28
29
29 def __init__(self, data, size):
30 def __init__(self, data: bytes, size):
30 chunks = size / len(data)
31 chunks = size // len(data)
31
32
32 self.stream = self._get_stream(data, chunks)
33 self.stream = self._get_stream(data, chunks)
33
34
34 def _get_stream(self, data, chunks):
35 def _get_stream(self, data, chunks):
35 for x in range(chunks):
36 for x in range(chunks):
36 yield data
37 yield data
37
38
38 def read(self, n):
39 def read(self, n):
39
40
40 buffer_stream = ''
41 buffer_stream = b''
41 for chunk in self.stream:
42 for chunk in self.stream:
42 buffer_stream += chunk
43 buffer_stream += chunk
43 if len(buffer_stream) >= n:
44 if len(buffer_stream) >= n:
44 break
45 break
45
46
46 # self.stream = self.bytes[n:]
47 # self.stream = self.bytes[n:]
47 return buffer_stream
48 return buffer_stream
48
49
49
50
50 @pytest.fixture(scope='module')
51 @pytest.fixture(scope='module')
51 def environ():
52 def environ():
52 """Delete coverage variables, as they make the tests fail."""
53 """Delete coverage variables, as they make the tests fail."""
53 env = dict(os.environ)
54 env = dict(os.environ)
54 for key in env.keys():
55 for key in env.keys():
55 if key.startswith('COV_CORE_'):
56 if key.startswith('COV_CORE_'):
56 del env[key]
57 del env[key]
57
58
58 return env
59 return env
59
60
60
61
61 def _get_python_args(script):
62 def _get_python_args(script):
62 return [sys.executable, '-c', 'import sys; import time; import shutil; ' + script]
63 return [sys.executable, '-c', 'import sys; import time; import shutil; ' + script]
63
64
64
65
65 def test_raise_exception_on_non_zero_return_code(environ):
66 def test_raise_exception_on_non_zero_return_code(environ):
66 args = _get_python_args('sys.exit(1)')
67 call_args = _get_python_args('raise ValueError("fail")')
67 with pytest.raises(EnvironmentError):
68 with pytest.raises(OSError):
68 list(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
69 b''.join(subprocessio.SubprocessIOChunker(call_args, shell=False, env=environ))
69
70
70
71
71 def test_does_not_fail_on_non_zero_return_code(environ):
72 def test_does_not_fail_on_non_zero_return_code(environ):
72 args = _get_python_args('sys.exit(1)')
73 call_args = _get_python_args('sys.stdout.write("hello"); sys.exit(1)')
73 output = ''.join(
74 proc = subprocessio.SubprocessIOChunker(call_args, shell=False, fail_on_return_code=False, env=environ)
74 subprocessio.SubprocessIOChunker(
75 output = b''.join(proc)
75 args, shell=False, fail_on_return_code=False, env=environ
76 )
77 )
78
76
79 assert output == ''
77 assert output == b'hello'
80
78
81
79
82 def test_raise_exception_on_stderr(environ):
80 def test_raise_exception_on_stderr(environ):
83 args = _get_python_args('sys.stderr.write("X"); time.sleep(1);')
81 call_args = _get_python_args('sys.stderr.write("WRITE_TO_STDERR"); time.sleep(1);')
84 with pytest.raises(EnvironmentError) as excinfo:
85 list(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
86
82
87 assert 'exited due to an error:\nX' in str(excinfo.value)
83 with pytest.raises(OSError) as excinfo:
84 b''.join(subprocessio.SubprocessIOChunker(call_args, shell=False, env=environ))
85
86 assert 'exited due to an error:\nWRITE_TO_STDERR' in str(excinfo.value)
88
87
89
88
90 def test_does_not_fail_on_stderr(environ):
89 def test_does_not_fail_on_stderr(environ):
91 args = _get_python_args('sys.stderr.write("X"); time.sleep(1);')
90 call_args = _get_python_args('sys.stderr.write("WRITE_TO_STDERR"); sys.stderr.flush; time.sleep(2);')
92 output = ''.join(
91 proc = subprocessio.SubprocessIOChunker(call_args, shell=False, fail_on_stderr=False, env=environ)
93 subprocessio.SubprocessIOChunker(
92 output = b''.join(proc)
94 args, shell=False, fail_on_stderr=False, env=environ
95 )
96 )
97
93
98 assert output == ''
94 assert output == b''
99
95
100
96
101 @pytest.mark.parametrize('size', [1, 10 ** 5])
97 @pytest.mark.parametrize('size', [
98 1,
99 10 ** 5
100 ])
102 def test_output_with_no_input(size, environ):
101 def test_output_with_no_input(size, environ):
103 print((type(environ)))
102 call_args = _get_python_args(f'sys.stdout.write("X" * {size});')
104 data = 'X'
103 proc = subprocessio.SubprocessIOChunker(call_args, shell=False, env=environ)
105 args = _get_python_args('sys.stdout.write("%s" * %d)' % (data, size))
104 output = b''.join(proc)
106 output = ''.join(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
107
105
108 assert output == data * size
106 assert output == ascii_bytes("X" * size)
109
107
110
108
111 @pytest.mark.parametrize('size', [1, 10 ** 5])
109 @pytest.mark.parametrize('size', [
110 1,
111 10 ** 5
112 ])
112 def test_output_with_no_input_does_not_fail(size, environ):
113 def test_output_with_no_input_does_not_fail(size, environ):
113 data = 'X'
114 args = _get_python_args('sys.stdout.write("%s" * %d); sys.exit(1)' % (data, size))
115 output = ''.join(
116 subprocessio.SubprocessIOChunker(
117 args, shell=False, fail_on_return_code=False, env=environ
118 )
119 )
120
114
121 print(("{} {}".format(len(data * size), len(output))))
115 call_args = _get_python_args(f'sys.stdout.write("X" * {size}); sys.exit(1)')
122 assert output == data * size
116 proc = subprocessio.SubprocessIOChunker(call_args, shell=False, fail_on_return_code=False, env=environ)
117 output = b''.join(proc)
118
119 assert output == ascii_bytes("X" * size)
123
120
124
121
125 @pytest.mark.parametrize('size', [1, 10 ** 5])
122 @pytest.mark.parametrize('size', [
123 1,
124 10 ** 5
125 ])
126 def test_output_with_input(size, environ):
126 def test_output_with_input(size, environ):
127 data_len = size
127 data_len = size
128 inputstream = KindaFilelike('X', size)
128 inputstream = FileLikeObj(b'X', size)
129
129
130 # This acts like the cat command.
130 # This acts like the cat command.
131 args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
131 call_args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
132 output = ''.join(
132 # note: in this tests we explicitly don't assign chunker to a variable and let it stream directly
133 subprocessio.SubprocessIOChunker(
133 output = b''.join(
134 args, shell=False, inputstream=inputstream, env=environ
134 subprocessio.SubprocessIOChunker(call_args, shell=False, input_stream=inputstream, env=environ)
135 )
136 )
135 )
137
136
138 assert len(output) == data_len
137 assert len(output) == data_len
139
138
140
139
141 @pytest.mark.parametrize('size', [1, 10 ** 5])
140 @pytest.mark.parametrize('size', [
141 1,
142 10 ** 5
143 ])
142 def test_output_with_input_skipping_iterator(size, environ):
144 def test_output_with_input_skipping_iterator(size, environ):
143 data_len = size
145 data_len = size
144 inputstream = KindaFilelike('X', size)
146 inputstream = FileLikeObj(b'X', size)
145
147
146 # This acts like the cat command.
148 # This acts like the cat command.
147 args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
149 call_args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
148
150
149 # Note: assigning the chunker makes sure that it is not deleted too early
151 # Note: assigning the chunker makes sure that it is not deleted too early
150 chunker = subprocessio.SubprocessIOChunker(
152 proc = subprocessio.SubprocessIOChunker(call_args, shell=False, input_stream=inputstream, env=environ)
151 args, shell=False, inputstream=inputstream, env=environ
153 output = b''.join(proc.stdout)
152 )
153 output = ''.join(chunker.output)
154
154
155 assert len(output) == data_len
155 assert len(output) == data_len
@@ -1,86 +1,103 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import io
18 import io
19 import mock
19 import mock
20 import pytest
20 import pytest
21 import sys
21 import sys
22
22
23 from vcsserver.utils import ascii_bytes
24
23
25
24 class MockPopen(object):
26 class MockPopen(object):
25 def __init__(self, stderr):
27 def __init__(self, stderr):
26 self.stdout = io.BytesIO('')
28 self.stdout = io.BytesIO(b'')
27 self.stderr = io.BytesIO(stderr)
29 self.stderr = io.BytesIO(stderr)
28 self.returncode = 1
30 self.returncode = 1
29
31
30 def wait(self):
32 def wait(self):
31 pass
33 pass
32
34
33
35
34 INVALID_CERTIFICATE_STDERR = '\n'.join([
36 INVALID_CERTIFICATE_STDERR = '\n'.join([
35 'svnrdump: E230001: Unable to connect to a repository at URL url',
37 'svnrdump: E230001: Unable to connect to a repository at URL url',
36 'svnrdump: E230001: Server SSL certificate verification failed: issuer is not trusted',
38 'svnrdump: E230001: Server SSL certificate verification failed: issuer is not trusted',
37 ])
39 ])
38
40
39
41
40 @pytest.mark.parametrize('stderr,expected_reason', [
42 @pytest.mark.parametrize('stderr,expected_reason', [
41 (INVALID_CERTIFICATE_STDERR, 'INVALID_CERTIFICATE'),
43 (INVALID_CERTIFICATE_STDERR, 'INVALID_CERTIFICATE'),
42 ('svnrdump: E123456', 'UNKNOWN:svnrdump: E123456'),
44 ('svnrdump: E123456', 'UNKNOWN:svnrdump: E123456'),
43 ], ids=['invalid-cert-stderr', 'svnrdump-err-123456'])
45 ], ids=['invalid-cert-stderr', 'svnrdump-err-123456'])
44 @pytest.mark.xfail(sys.platform == "cygwin",
46 @pytest.mark.xfail(sys.platform == "cygwin",
45 reason="SVN not packaged for Cygwin")
47 reason="SVN not packaged for Cygwin")
46 def test_import_remote_repository_certificate_error(stderr, expected_reason):
48 def test_import_remote_repository_certificate_error(stderr, expected_reason):
47 from vcsserver.remote import svn
49 from vcsserver.remote import svn
48 factory = mock.Mock()
50 factory = mock.Mock()
49 factory.repo = mock.Mock(return_value=mock.Mock())
51 factory.repo = mock.Mock(return_value=mock.Mock())
50
52
51 remote = svn.SvnRemote(factory)
53 remote = svn.SvnRemote(factory)
52 remote.is_path_valid_repository = lambda wire, path: True
54 remote.is_path_valid_repository = lambda wire, path: True
53
55
54 with mock.patch('subprocess.Popen',
56 with mock.patch('subprocess.Popen',
55 return_value=MockPopen(stderr)):
57 return_value=MockPopen(ascii_bytes(stderr))):
56 with pytest.raises(Exception) as excinfo:
58 with pytest.raises(Exception) as excinfo:
57 remote.import_remote_repository({'path': 'path'}, 'url')
59 remote.import_remote_repository({'path': 'path'}, 'url')
58
60
59 expected_error_args = (
61 expected_error_args = 'Failed to dump the remote repository from url. Reason:{}'.format(expected_reason)
60 'Failed to dump the remote repository from url. Reason:{}'.format(expected_reason),)
61
62
62 assert excinfo.value.args == expected_error_args
63 assert excinfo.value.args[0] == expected_error_args
63
64
64
65
65 def test_svn_libraries_can_be_imported():
66 def test_svn_libraries_can_be_imported():
66 import svn.client
67 import svn.client
67 assert svn.client is not None
68 assert svn.client is not None
68
69
69
70
70 @pytest.mark.parametrize('example_url, parts', [
71 @pytest.mark.parametrize('example_url, parts', [
71 ('http://server.com', (None, None, 'http://server.com')),
72 ('http://server.com', (None, None, 'http://server.com')),
72 ('http://user@server.com', ('user', None, 'http://user@server.com')),
73 ('http://user@server.com', ('user', None, 'http://user@server.com')),
73 ('http://user:pass@server.com', ('user', 'pass', 'http://user:pass@server.com')),
74 ('http://user:pass@server.com', ('user', 'pass', 'http://user:pass@server.com')),
74 ('<script>', (None, None, '<script>')),
75 ('<script>', (None, None, '<script>')),
75 ('http://', (None, None, 'http://')),
76 ('http://', (None, None, 'http://')),
76 ])
77 ])
77 def test_username_password_extraction_from_url(example_url, parts):
78 def test_username_password_extraction_from_url(example_url, parts):
78 from vcsserver.remote import svn
79 from vcsserver.remote import svn
79
80
80 factory = mock.Mock()
81 factory = mock.Mock()
81 factory.repo = mock.Mock(return_value=mock.Mock())
82 factory.repo = mock.Mock(return_value=mock.Mock())
82
83
83 remote = svn.SvnRemote(factory)
84 remote = svn.SvnRemote(factory)
84 remote.is_path_valid_repository = lambda wire, path: True
85 remote.is_path_valid_repository = lambda wire, path: True
85
86
86 assert remote.get_url_and_credentials(example_url) == parts
87 assert remote.get_url_and_credentials(example_url) == parts
88
89
90 @pytest.mark.parametrize('call_url', [
91 b'https://svn.code.sf.net/p/svnbook/source/trunk/',
92 b'https://marcink@svn.code.sf.net/p/svnbook/source/trunk/',
93 b'https://marcink:qweqwe@svn.code.sf.net/p/svnbook/source/trunk/',
94 ])
95 def test_check_url(call_url):
96 from vcsserver.remote import svn
97 factory = mock.Mock()
98 factory.repo = mock.Mock(return_value=mock.Mock())
99
100 remote = svn.SvnRemote(factory)
101 remote.is_path_valid_repository = lambda wire, path: True
102 assert remote.check_url(call_url)
103
@@ -1,96 +1,98 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import wsgiref.simple_server
18 import wsgiref.simple_server
19 import wsgiref.validate
19 import wsgiref.validate
20
20
21 from vcsserver import wsgi_app_caller
21 from vcsserver import wsgi_app_caller
22
22 from vcsserver.utils import ascii_bytes, safe_str
23
24 # pylint: disable=protected-access,too-many-public-methods
25
23
26
24
27 @wsgiref.validate.validator
25 @wsgiref.validate.validator
28 def demo_app(environ, start_response):
26 def demo_app(environ, start_response):
29 """WSGI app used for testing."""
27 """WSGI app used for testing."""
28
29 input_data = safe_str(environ['wsgi.input'].read(1024))
30
30 data = [
31 data = [
31 'Hello World!\n',
32 f'Hello World!\n',
32 'input_data=%s\n' % environ['wsgi.input'].read(),
33 f'input_data={input_data}\n',
33 ]
34 ]
34 for key, value in sorted(environ.items()):
35 for key, value in sorted(environ.items()):
35 data.append('%s=%s\n' % (key, value))
36 data.append(f'{key}={value}\n')
36
37
37 write = start_response("200 OK", [('Content-Type', 'text/plain')])
38 write = start_response("200 OK", [('Content-Type', 'text/plain')])
38 write('Old school write method\n')
39 write(b'Old school write method\n')
39 write('***********************\n')
40 write(b'***********************\n')
40 return data
41 return list(map(ascii_bytes, data))
41
42
42
43
43 BASE_ENVIRON = {
44 BASE_ENVIRON = {
44 'REQUEST_METHOD': 'GET',
45 'REQUEST_METHOD': 'GET',
45 'SERVER_NAME': 'localhost',
46 'SERVER_NAME': 'localhost',
46 'SERVER_PORT': '80',
47 'SERVER_PORT': '80',
47 'SCRIPT_NAME': '',
48 'SCRIPT_NAME': '',
48 'PATH_INFO': '/',
49 'PATH_INFO': '/',
49 'QUERY_STRING': '',
50 'QUERY_STRING': '',
50 'foo.var': 'bla',
51 'foo.var': 'bla',
51 }
52 }
52
53
53
54
54 def test_complete_environ():
55 def test_complete_environ():
55 environ = dict(BASE_ENVIRON)
56 environ = dict(BASE_ENVIRON)
56 data = "data"
57 data = b"data"
57 wsgi_app_caller._complete_environ(environ, data)
58 wsgi_app_caller._complete_environ(environ, data)
58 wsgiref.validate.check_environ(environ)
59 wsgiref.validate.check_environ(environ)
59
60
60 assert data == environ['wsgi.input'].read()
61 assert data == environ['wsgi.input'].read(1024)
61
62
62
63
63 def test_start_response():
64 def test_start_response():
64 start_response = wsgi_app_caller._StartResponse()
65 start_response = wsgi_app_caller._StartResponse()
65 status = '200 OK'
66 status = '200 OK'
66 headers = [('Content-Type', 'text/plain')]
67 headers = [('Content-Type', 'text/plain')]
67 start_response(status, headers)
68 start_response(status, headers)
68
69
69 assert status == start_response.status
70 assert status == start_response.status
70 assert headers == start_response.headers
71 assert headers == start_response.headers
71
72
72
73
73 def test_start_response_with_error():
74 def test_start_response_with_error():
74 start_response = wsgi_app_caller._StartResponse()
75 start_response = wsgi_app_caller._StartResponse()
75 status = '500 Internal Server Error'
76 status = '500 Internal Server Error'
76 headers = [('Content-Type', 'text/plain')]
77 headers = [('Content-Type', 'text/plain')]
77 start_response(status, headers, (None, None, None))
78 start_response(status, headers, (None, None, None))
78
79
79 assert status == start_response.status
80 assert status == start_response.status
80 assert headers == start_response.headers
81 assert headers == start_response.headers
81
82
82
83
83 def test_wsgi_app_caller():
84 def test_wsgi_app_caller():
84 caller = wsgi_app_caller.WSGIAppCaller(demo_app)
85 environ = dict(BASE_ENVIRON)
85 environ = dict(BASE_ENVIRON)
86 input_data = 'some text'
86 input_data = 'some text'
87
88 caller = wsgi_app_caller.WSGIAppCaller(demo_app)
87 responses, status, headers = caller.handle(environ, input_data)
89 responses, status, headers = caller.handle(environ, input_data)
88 response = ''.join(responses)
90 response = b''.join(responses)
89
91
90 assert status == '200 OK'
92 assert status == '200 OK'
91 assert headers == [('Content-Type', 'text/plain')]
93 assert headers == [('Content-Type', 'text/plain')]
92 assert response.startswith(
94 assert response.startswith(b'Old school write method\n***********************\n')
93 'Old school write method\n***********************\n')
95 assert b'Hello World!\n' in response
94 assert 'Hello World!\n' in response
96 assert b'foo.var=bla\n' in response
95 assert 'foo.var=bla\n' in response
97
96 assert 'input_data=%s\n' % input_data in response
98 assert ascii_bytes(f'input_data={input_data}\n') in response
@@ -1,107 +1,106 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import time
18 import time
19 import logging
19 import logging
20
20
21 import vcsserver
21 import vcsserver
22 from vcsserver.utils import safe_str
22 from vcsserver.utils import safe_str, ascii_str
23
24
23
25 log = logging.getLogger(__name__)
24 log = logging.getLogger(__name__)
26
25
27
26
28 def get_access_path(environ):
27 def get_access_path(environ):
29 path = environ.get('PATH_INFO')
28 path = environ.get('PATH_INFO')
30 return path
29 return path
31
30
32
31
33 def get_user_agent(environ):
32 def get_user_agent(environ):
34 return environ.get('HTTP_USER_AGENT')
33 return environ.get('HTTP_USER_AGENT')
35
34
36
35
37 def get_vcs_method(environ):
36 def get_vcs_method(environ):
38 return environ.get('HTTP_X_RC_METHOD')
37 return environ.get('HTTP_X_RC_METHOD')
39
38
40
39
41 def get_vcs_repo(environ):
40 def get_vcs_repo(environ):
42 return environ.get('HTTP_X_RC_REPO_NAME')
41 return environ.get('HTTP_X_RC_REPO_NAME')
43
42
44
43
45 class RequestWrapperTween(object):
44 class RequestWrapperTween(object):
46 def __init__(self, handler, registry):
45 def __init__(self, handler, registry):
47 self.handler = handler
46 self.handler = handler
48 self.registry = registry
47 self.registry = registry
49
48
50 # one-time configuration code goes here
49 # one-time configuration code goes here
51
50
52 def __call__(self, request):
51 def __call__(self, request):
53 start = time.time()
52 start = time.time()
54 log.debug('Starting request time measurement')
53 log.debug('Starting request time measurement')
55 response = None
54 response = None
56
55
57 ua = get_user_agent(request.environ)
56 ua = get_user_agent(request.environ)
58 vcs_method = get_vcs_method(request.environ)
57 vcs_method = get_vcs_method(request.environ)
59 repo_name = get_vcs_repo(request.environ)
58 repo_name = get_vcs_repo(request.environ)
60
59
61 try:
60 try:
62 response = self.handler(request)
61 response = self.handler(request)
63 finally:
62 finally:
64 count = request.request_count()
63 count = request.request_count()
65 _ver_ = vcsserver.__version__
64 _ver_ = ascii_str(vcsserver.__version__)
66 _path = safe_str(get_access_path(request.environ))
65 _path = safe_str(get_access_path(request.environ))
67 ip = '127.0.0.1'
66 ip = '127.0.0.1'
68 match_route = request.matched_route.name if request.matched_route else "NOT_FOUND"
67 match_route = request.matched_route.name if request.matched_route else "NOT_FOUND"
69 resp_code = getattr(response, 'status_code', 'UNDEFINED')
68 resp_code = getattr(response, 'status_code', 'UNDEFINED')
70
69
71 total = time.time() - start
70 total = time.time() - start
72
71
73 _view_path = "{}/{}@{}".format(_path, vcs_method, repo_name)
72 _view_path = f"{repo_name}@{_path}/{vcs_method}"
74 log.info(
73 log.info(
75 'Req[%4s] IP: %s %s Request to %s time: %.4fs [%s], VCSServer %s',
74 'Req[%4s] IP: %s %s Request to %s time: %.4fs [%s], VCSServer %s',
76 count, ip, request.environ.get('REQUEST_METHOD'),
75 count, ip, request.environ.get('REQUEST_METHOD'),
77 _view_path, total, ua, _ver_,
76 _view_path, total, ua, _ver_,
78 extra={"time": total, "ver": _ver_, "code": resp_code,
77 extra={"time": total, "ver": _ver_, "code": resp_code,
79 "path": _path, "view_name": match_route, "user_agent": ua,
78 "path": _path, "view_name": match_route, "user_agent": ua,
80 "vcs_method": vcs_method, "repo_name": repo_name}
79 "vcs_method": vcs_method, "repo_name": repo_name}
81 )
80 )
82
81
83 statsd = request.registry.statsd
82 statsd = request.registry.statsd
84 if statsd:
83 if statsd:
85 match_route = request.matched_route.name if request.matched_route else _path
84 match_route = request.matched_route.name if request.matched_route else _path
86 elapsed_time_ms = round(1000.0 * total) # use ms only
85 elapsed_time_ms = round(1000.0 * total) # use ms only
87 statsd.timing(
86 statsd.timing(
88 "vcsserver_req_timing.histogram", elapsed_time_ms,
87 "vcsserver_req_timing.histogram", elapsed_time_ms,
89 tags=[
88 tags=[
90 "view_name:{}".format(match_route),
89 "view_name:{}".format(match_route),
91 "code:{}".format(resp_code)
90 "code:{}".format(resp_code)
92 ],
91 ],
93 use_decimals=False
92 use_decimals=False
94 )
93 )
95 statsd.incr(
94 statsd.incr(
96 "vcsserver_req_total", tags=[
95 "vcsserver_req_total", tags=[
97 "view_name:{}".format(match_route),
96 "view_name:{}".format(match_route),
98 "code:{}".format(resp_code)
97 "code:{}".format(resp_code)
99 ])
98 ])
100
99
101 return response
100 return response
102
101
103
102
104 def includeme(config):
103 def includeme(config):
105 config.add_tween(
104 config.add_tween(
106 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
105 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
107 )
106 )
@@ -1,107 +1,137 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 import logging
17 import logging
18 import hashlib
18 import hashlib
19
19
20 log = logging.getLogger(__name__)
20 log = logging.getLogger(__name__)
21
21
22
22
23 def safe_int(val, default=None):
23 def safe_int(val, default=None):
24 """
24 """
25 Returns int() of val if val is not convertable to int use default
25 Returns int() of val if val is not convertable to int use default
26 instead
26 instead
27
27
28 :param val:
28 :param val:
29 :param default:
29 :param default:
30 """
30 """
31
31
32 try:
32 try:
33 val = int(val)
33 val = int(val)
34 except (ValueError, TypeError):
34 except (ValueError, TypeError):
35 val = default
35 val = default
36
36
37 return val
37 return val
38
38
39
39
40 def safe_str(str_, to_encoding=None) -> str:
40 def safe_str(str_, to_encoding=None) -> str:
41 """
41 """
42 safe str function. Does few trick to turn unicode_ into string
42 safe str function. Does few trick to turn unicode_ into string
43
43
44 :param str_: str to encode
44 :param str_: str to encode
45 :param to_encoding: encode to this type UTF8 default
45 :param to_encoding: encode to this type UTF8 default
46 :rtype: str
46 :rtype: str
47 :returns: str object
47 :returns: str object
48 """
48 """
49 if isinstance(str_, str):
49 if isinstance(str_, str):
50 return str_
50 return str_
51
51
52 # if it's bytes cast to str
52 # if it's bytes cast to str
53 if not isinstance(str_, bytes):
53 if not isinstance(str_, bytes):
54 return str(str_)
54 return str(str_)
55
55
56 to_encoding = to_encoding or ['utf8']
56 to_encoding = to_encoding or ['utf8']
57 if not isinstance(to_encoding, (list, tuple)):
57 if not isinstance(to_encoding, (list, tuple)):
58 to_encoding = [to_encoding]
58 to_encoding = [to_encoding]
59
59
60 for enc in to_encoding:
60 for enc in to_encoding:
61 try:
61 try:
62 return str(str_, enc)
62 return str(str_, enc)
63 except UnicodeDecodeError:
63 except UnicodeDecodeError:
64 pass
64 pass
65
65
66 return str(str_, to_encoding[0], 'replace')
66 return str(str_, to_encoding[0], 'replace')
67
67
68
68
69 def safe_bytes(str_, from_encoding=None) -> bytes:
69 def safe_bytes(str_, from_encoding=None) -> bytes:
70 """
70 """
71 safe bytes function. Does few trick to turn str_ into bytes string:
71 safe bytes function. Does few trick to turn str_ into bytes string:
72
72
73 :param str_: string to decode
73 :param str_: string to decode
74 :param from_encoding: encode from this type UTF8 default
74 :param from_encoding: encode from this type UTF8 default
75 :rtype: unicode
75 :rtype: unicode
76 :returns: unicode object
76 :returns: unicode object
77 """
77 """
78 if isinstance(str_, bytes):
78 if isinstance(str_, bytes):
79 return str_
79 return str_
80
80
81 if not isinstance(str_, str):
81 if not isinstance(str_, str):
82 raise ValueError('safe_bytes cannot convert other types than str: got: {}'.format(type(str_)))
82 raise ValueError('safe_bytes cannot convert other types than str: got: {}'.format(type(str_)))
83
83
84 from_encoding = from_encoding or ['utf8']
84 from_encoding = from_encoding or ['utf8']
85 if not isinstance(from_encoding, (list, tuple)):
85 if not isinstance(from_encoding, (list, tuple)):
86 from_encoding = [from_encoding]
86 from_encoding = [from_encoding]
87
87
88 for enc in from_encoding:
88 for enc in from_encoding:
89 try:
89 try:
90 return str_.encode(enc)
90 return str_.encode(enc)
91 except UnicodeDecodeError:
91 except UnicodeDecodeError:
92 pass
92 pass
93
93
94 return unicode(str_, from_encoding[0], 'replace')
94 return str_.encode(from_encoding[0], 'replace')
95
96
97 def ascii_bytes(str_, allow_bytes=False) -> bytes:
98 """
99 Simple conversion from str to bytes, with assumption that str_ is pure ASCII.
100 Fails with UnicodeError on invalid input.
101 This should be used where encoding and "safe" ambiguity should be avoided.
102 Where strings already have been encoded in other ways but still are unicode
103 string - for example to hex, base64, json, urlencoding, or are known to be
104 identifiers.
105 """
106 if allow_bytes and isinstance(str_, bytes):
107 return str_
108
109 if not isinstance(str_, str):
110 raise ValueError('ascii_bytes cannot convert other types than str: got: {}'.format(type(str_)))
111 return str_.encode('ascii')
112
113
114 def ascii_str(str_):
115 """
116 Simple conversion from bytes to str, with assumption that str_ is pure ASCII.
117 Fails with UnicodeError on invalid input.
118 This should be used where encoding and "safe" ambiguity should be avoided.
119 Where strings are encoded but also in other ways are known to be ASCII, and
120 where a unicode string is wanted without caring about encoding. For example
121 to hex, base64, urlencoding, or are known to be identifiers.
122 """
123
124 if not isinstance(str_, bytes):
125 raise ValueError('ascii_str cannot convert other types than bytes: got: {}'.format(type(str_)))
126 return str_.decode('ascii')
95
127
96
128
97 class AttributeDict(dict):
129 class AttributeDict(dict):
98 def __getattr__(self, attr):
130 def __getattr__(self, attr):
99 return self.get(attr, None)
131 return self.get(attr, None)
100 __setattr__ = dict.__setitem__
132 __setattr__ = dict.__setitem__
101 __delattr__ = dict.__delitem__
133 __delattr__ = dict.__delitem__
102
134
103
135
104 def sha1(val):
136 def sha1(val):
105 return hashlib.sha1(val).hexdigest()
137 return hashlib.sha1(val).hexdigest()
106
107
@@ -1,46 +1,47 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 from vcsserver.lib import rc_cache
18 from vcsserver.lib import rc_cache
19
19
20
20 class RemoteBase(object):
21 class RemoteBase(object):
21 EMPTY_COMMIT = '0' * 40
22 EMPTY_COMMIT = '0' * 40
22
23
23 def _region(self, wire):
24 def _region(self, wire):
24 cache_repo_id = wire.get('cache_repo_id', '')
25 cache_repo_id = wire.get('cache_repo_id', '')
25 cache_namespace_uid = 'cache_repo.{}'.format(cache_repo_id)
26 cache_namespace_uid = 'cache_repo.{}'.format(cache_repo_id)
26 return rc_cache.get_or_create_region('repo_object', cache_namespace_uid)
27 return rc_cache.get_or_create_region('repo_object', cache_namespace_uid)
27
28
28 def _cache_on(self, wire):
29 def _cache_on(self, wire):
29 context = wire.get('context', '')
30 context = wire.get('context', '')
30 context_uid = '{}'.format(context)
31 context_uid = '{}'.format(context)
31 repo_id = wire.get('repo_id', '')
32 repo_id = wire.get('repo_id', '')
32 cache = wire.get('cache', True)
33 cache = wire.get('cache', True)
33 cache_on = context and cache
34 cache_on = context and cache
34 return cache_on, context_uid, repo_id
35 return cache_on, context_uid, repo_id
35
36
36 def vcsserver_invalidate_cache(self, wire, delete):
37 def vcsserver_invalidate_cache(self, wire, delete):
37 from vcsserver.lib import rc_cache
38 from vcsserver.lib import rc_cache
38 repo_id = wire.get('repo_id', '')
39 repo_id = wire.get('repo_id', '')
39 cache_repo_id = wire.get('cache_repo_id', '')
40 cache_repo_id = wire.get('cache_repo_id', '')
40 cache_namespace_uid = 'cache_repo.{}'.format(cache_repo_id)
41 cache_namespace_uid = 'cache_repo.{}'.format(cache_repo_id)
41
42
42 if delete:
43 if delete:
43 rc_cache.clear_cache_namespace(
44 rc_cache.clear_cache_namespace(
44 'repo_object', cache_namespace_uid, invalidate=True)
45 'repo_object', cache_namespace_uid, invalidate=True)
45
46
46 return {'invalidated': {'repo_id': repo_id, 'delete': delete}}
47 return {'invalidated': {'repo_id': repo_id, 'delete': delete}}
@@ -1,116 +1,116 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 """Extract the responses of a WSGI app."""
18 """Extract the responses of a WSGI app."""
19
19
20 __all__ = ('WSGIAppCaller',)
20 __all__ = ('WSGIAppCaller',)
21
21
22 import io
22 import io
23 import logging
23 import logging
24 import os
24 import os
25
25
26 from vcsserver.utils import ascii_bytes
26
27
27 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
28
29
29 DEV_NULL = open(os.devnull)
30 DEV_NULL = open(os.devnull)
30
31
31
32
32 def _complete_environ(environ, input_data):
33 def _complete_environ(environ, input_data: bytes):
33 """Update the missing wsgi.* variables of a WSGI environment.
34 """Update the missing wsgi.* variables of a WSGI environment.
34
35
35 :param environ: WSGI environment to update
36 :param environ: WSGI environment to update
36 :type environ: dict
37 :type environ: dict
37 :param input_data: data to be read by the app
38 :param input_data: data to be read by the app
38 :type input_data: str
39 :type input_data: bytes
39 """
40 """
40 environ.update({
41 environ.update({
41 'wsgi.version': (1, 0),
42 'wsgi.version': (1, 0),
42 'wsgi.url_scheme': 'http',
43 'wsgi.url_scheme': 'http',
43 'wsgi.multithread': True,
44 'wsgi.multithread': True,
44 'wsgi.multiprocess': True,
45 'wsgi.multiprocess': True,
45 'wsgi.run_once': False,
46 'wsgi.run_once': False,
46 'wsgi.input': io.BytesIO(input_data),
47 'wsgi.input': io.BytesIO(input_data),
47 'wsgi.errors': DEV_NULL,
48 'wsgi.errors': DEV_NULL,
48 })
49 })
49
50
50
51
51 # pylint: disable=too-few-public-methods
52 # pylint: disable=too-few-public-methods
52 class _StartResponse(object):
53 class _StartResponse(object):
53 """Save the arguments of a start_response call."""
54 """Save the arguments of a start_response call."""
54
55
55 __slots__ = ['status', 'headers', 'content']
56 __slots__ = ['status', 'headers', 'content']
56
57
57 def __init__(self):
58 def __init__(self):
58 self.status = None
59 self.status = None
59 self.headers = None
60 self.headers = None
60 self.content = []
61 self.content = []
61
62
62 def __call__(self, status, headers, exc_info=None):
63 def __call__(self, status, headers, exc_info=None):
63 # TODO(skreft): do something meaningful with the exc_info
64 # TODO(skreft): do something meaningful with the exc_info
64 exc_info = None # avoid dangling circular reference
65 exc_info = None # avoid dangling circular reference
65 self.status = status
66 self.status = status
66 self.headers = headers
67 self.headers = headers
67
68
68 return self.write
69 return self.write
69
70
70 def write(self, content):
71 def write(self, content):
71 """Write method returning when calling this object.
72 """Write method returning when calling this object.
72
73
73 All the data written is then available in content.
74 All the data written is then available in content.
74 """
75 """
75 self.content.append(content)
76 self.content.append(content)
76
77
77
78
78 class WSGIAppCaller(object):
79 class WSGIAppCaller(object):
79 """Calls a WSGI app."""
80 """Calls a WSGI app."""
80
81
81 def __init__(self, app):
82 def __init__(self, app):
82 """
83 """
83 :param app: WSGI app to call
84 :param app: WSGI app to call
84 """
85 """
85 self.app = app
86 self.app = app
86
87
87 def handle(self, environ, input_data):
88 def handle(self, environ, input_data):
88 """Process a request with the WSGI app.
89 """Process a request with the WSGI app.
89
90
90 The returned data of the app is fully consumed into a list.
91 The returned data of the app is fully consumed into a list.
91
92
92 :param environ: WSGI environment to update
93 :param environ: WSGI environment to update
93 :type environ: dict
94 :type environ: dict
94 :param input_data: data to be read by the app
95 :param input_data: data to be read by the app
95 :type input_data: str
96 :type input_data: str/bytes
96
97
97 :returns: a tuple with the contents, status and headers
98 :returns: a tuple with the contents, status and headers
98 :rtype: (list<str>, str, list<(str, str)>)
99 :rtype: (list<str>, str, list<(str, str)>)
99 """
100 """
100 _complete_environ(environ, input_data)
101 _complete_environ(environ, ascii_bytes(input_data, allow_bytes=True))
101 start_response = _StartResponse()
102 start_response = _StartResponse()
102 log.debug("Calling wrapped WSGI application")
103 log.debug("Calling wrapped WSGI application")
103 responses = self.app(environ, start_response)
104 responses = self.app(environ, start_response)
104 responses_list = list(responses)
105 responses_list = list(responses)
105 existing_responses = start_response.content
106 existing_responses = start_response.content
106 if existing_responses:
107 if existing_responses:
107 log.debug(
108 log.debug("Adding returned response to response written via write()")
108 "Adding returned response to response written via write()")
109 existing_responses.extend(responses_list)
109 existing_responses.extend(responses_list)
110 responses_list = existing_responses
110 responses_list = existing_responses
111 if hasattr(responses, 'close'):
111 if hasattr(responses, 'close'):
112 log.debug("Closing iterator from WSGI application")
112 log.debug("Closing iterator from WSGI application")
113 responses.close()
113 responses.close()
114
114
115 log.debug("Handling of WSGI request done, returning response")
115 log.debug("Handling of WSGI request done, returning response")
116 return responses_list, start_response.status, start_response.headers
116 return responses_list, start_response.status, start_response.headers
General Comments 0
You need to be logged in to leave comments. Login now