##// END OF EJS Templates
core: various cleanups and fixes
super-admin -
r1118:c35e966b python3
parent child Browse files
Show More
@@ -0,0 +1,27 b''
1 # Copyright (C) 2010-2020 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 HOOK_REPO_SIZE = 'changegroup.repo_size'
20
21 # HG
22 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
23 HOOK_PULL = 'outgoing.pull_logger'
24 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
25 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
26 HOOK_PUSH = 'changegroup.push_logger'
27 HOOK_PUSH_KEY = 'pushkey.key_push'
@@ -1,170 +1,168 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
1 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import os
19 import os
22 import textwrap
20 import textwrap
23 import string
21 import string
24 import functools
22 import functools
25 import logging
23 import logging
26 import tempfile
24 import tempfile
27 import logging.config
25 import logging.config
28
26
29 from vcsserver.type_utils import str2bool, aslist
27 from vcsserver.type_utils import str2bool, aslist
30
28
31 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
32
30
33 # skip keys, that are set here, so we don't double process those
31 # skip keys, that are set here, so we don't double process those
34 set_keys = {
32 set_keys = {
35 '__file__': ''
33 '__file__': ''
36 }
34 }
37
35
38
36
39 class SettingsMaker(object):
37 class SettingsMaker(object):
40
38
41 def __init__(self, app_settings):
39 def __init__(self, app_settings):
42 self.settings = app_settings
40 self.settings = app_settings
43
41
44 @classmethod
42 @classmethod
45 def _bool_func(cls, input_val):
43 def _bool_func(cls, input_val):
46 if isinstance(input_val, bytes):
44 if isinstance(input_val, bytes):
47 # decode to str
45 # decode to str
48 input_val = input_val.decode('utf8')
46 input_val = input_val.decode('utf8')
49 return str2bool(input_val)
47 return str2bool(input_val)
50
48
51 @classmethod
49 @classmethod
52 def _int_func(cls, input_val):
50 def _int_func(cls, input_val):
53 return int(input_val)
51 return int(input_val)
54
52
55 @classmethod
53 @classmethod
56 def _list_func(cls, input_val, sep=','):
54 def _list_func(cls, input_val, sep=','):
57 return aslist(input_val, sep=sep)
55 return aslist(input_val, sep=sep)
58
56
59 @classmethod
57 @classmethod
60 def _string_func(cls, input_val, lower=True):
58 def _string_func(cls, input_val, lower=True):
61 if lower:
59 if lower:
62 input_val = input_val.lower()
60 input_val = input_val.lower()
63 return input_val
61 return input_val
64
62
65 @classmethod
63 @classmethod
66 def _float_func(cls, input_val):
64 def _float_func(cls, input_val):
67 return float(input_val)
65 return float(input_val)
68
66
69 @classmethod
67 @classmethod
70 def _dir_func(cls, input_val, ensure_dir=False, mode=0o755):
68 def _dir_func(cls, input_val, ensure_dir=False, mode=0o755):
71
69
72 # ensure we have our dir created
70 # ensure we have our dir created
73 if not os.path.isdir(input_val) and ensure_dir:
71 if not os.path.isdir(input_val) and ensure_dir:
74 os.makedirs(input_val, mode=mode)
72 os.makedirs(input_val, mode=mode, exist_ok=True)
75
73
76 if not os.path.isdir(input_val):
74 if not os.path.isdir(input_val):
77 raise Exception('Dir at {} does not exist'.format(input_val))
75 raise Exception('Dir at {} does not exist'.format(input_val))
78 return input_val
76 return input_val
79
77
80 @classmethod
78 @classmethod
81 def _file_path_func(cls, input_val, ensure_dir=False, mode=0o755):
79 def _file_path_func(cls, input_val, ensure_dir=False, mode=0o755):
82 dirname = os.path.dirname(input_val)
80 dirname = os.path.dirname(input_val)
83 cls._dir_func(dirname, ensure_dir=ensure_dir)
81 cls._dir_func(dirname, ensure_dir=ensure_dir)
84 return input_val
82 return input_val
85
83
86 @classmethod
84 @classmethod
87 def _key_transformator(cls, key):
85 def _key_transformator(cls, key):
88 return "{}_{}".format('RC'.upper(), key.upper().replace('.', '_').replace('-', '_'))
86 return "{}_{}".format('RC'.upper(), key.upper().replace('.', '_').replace('-', '_'))
89
87
90 def maybe_env_key(self, key):
88 def maybe_env_key(self, key):
91 # now maybe we have this KEY in env, search and use the value with higher priority.
89 # now maybe we have this KEY in env, search and use the value with higher priority.
92 transformed_key = self._key_transformator(key)
90 transformed_key = self._key_transformator(key)
93 envvar_value = os.environ.get(transformed_key)
91 envvar_value = os.environ.get(transformed_key)
94 if envvar_value:
92 if envvar_value:
95 log.debug('using `%s` key instead of `%s` key for config', transformed_key, key)
93 log.debug('using `%s` key instead of `%s` key for config', transformed_key, key)
96
94
97 return envvar_value
95 return envvar_value
98
96
99 def env_expand(self):
97 def env_expand(self):
100 replaced = {}
98 replaced = {}
101 for k, v in self.settings.items():
99 for k, v in self.settings.items():
102 if k not in set_keys:
100 if k not in set_keys:
103 envvar_value = self.maybe_env_key(k)
101 envvar_value = self.maybe_env_key(k)
104 if envvar_value:
102 if envvar_value:
105 replaced[k] = envvar_value
103 replaced[k] = envvar_value
106 set_keys[k] = envvar_value
104 set_keys[k] = envvar_value
107
105
108 # replace ALL keys updated
106 # replace ALL keys updated
109 self.settings.update(replaced)
107 self.settings.update(replaced)
110
108
111 def enable_logging(self, logging_conf=None, level='INFO', formatter='generic'):
109 def enable_logging(self, logging_conf=None, level='INFO', formatter='generic'):
112 """
110 """
113 Helper to enable debug on running instance
111 Helper to enable debug on running instance
114 :return:
112 :return:
115 """
113 """
116
114
117 if not str2bool(self.settings.get('logging.autoconfigure')):
115 if not str2bool(self.settings.get('logging.autoconfigure')):
118 log.info('logging configuration based on main .ini file')
116 log.info('logging configuration based on main .ini file')
119 return
117 return
120
118
121 if logging_conf is None:
119 if logging_conf is None:
122 logging_conf = self.settings.get('logging.logging_conf_file') or ''
120 logging_conf = self.settings.get('logging.logging_conf_file') or ''
123
121
124 if not os.path.isfile(logging_conf):
122 if not os.path.isfile(logging_conf):
125 log.error('Unable to setup logging based on %s, '
123 log.error('Unable to setup logging based on %s, '
126 'file does not exist.... specify path using logging.logging_conf_file= config setting. ', logging_conf)
124 'file does not exist.... specify path using logging.logging_conf_file= config setting. ', logging_conf)
127 return
125 return
128
126
129 with open(logging_conf, 'rt') as f:
127 with open(logging_conf, 'rt') as f:
130 ini_template = textwrap.dedent(f.read())
128 ini_template = textwrap.dedent(f.read())
131 ini_template = string.Template(ini_template).safe_substitute(
129 ini_template = string.Template(ini_template).safe_substitute(
132 RC_LOGGING_LEVEL=os.environ.get('RC_LOGGING_LEVEL', '') or level,
130 RC_LOGGING_LEVEL=os.environ.get('RC_LOGGING_LEVEL', '') or level,
133 RC_LOGGING_FORMATTER=os.environ.get('RC_LOGGING_FORMATTER', '') or formatter
131 RC_LOGGING_FORMATTER=os.environ.get('RC_LOGGING_FORMATTER', '') or formatter
134 )
132 )
135
133
136 with tempfile.NamedTemporaryFile(prefix='rc_logging_', suffix='.ini', delete=False) as f:
134 with tempfile.NamedTemporaryFile(prefix='rc_logging_', suffix='.ini', delete=False) as f:
137 log.info('Saved Temporary LOGGING config at %s', f.name)
135 log.info('Saved Temporary LOGGING config at %s', f.name)
138 f.write(ini_template)
136 f.write(ini_template)
139
137
140 logging.config.fileConfig(f.name)
138 logging.config.fileConfig(f.name)
141 os.remove(f.name)
139 os.remove(f.name)
142
140
143 def make_setting(self, key, default, lower=False, default_when_empty=False, parser=None):
141 def make_setting(self, key, default, lower=False, default_when_empty=False, parser=None):
144 input_val = self.settings.get(key, default)
142 input_val = self.settings.get(key, default)
145
143
146 if default_when_empty and not input_val:
144 if default_when_empty and not input_val:
147 # use default value when value is set in the config but it is empty
145 # use default value when value is set in the config but it is empty
148 input_val = default
146 input_val = default
149
147
150 parser_func = {
148 parser_func = {
151 'bool': self._bool_func,
149 'bool': self._bool_func,
152 'int': self._int_func,
150 'int': self._int_func,
153 'list': self._list_func,
151 'list': self._list_func,
154 'list:newline': functools.partial(self._list_func, sep='/n'),
152 'list:newline': functools.partial(self._list_func, sep='/n'),
155 'list:spacesep': functools.partial(self._list_func, sep=' '),
153 'list:spacesep': functools.partial(self._list_func, sep=' '),
156 'string': functools.partial(self._string_func, lower=lower),
154 'string': functools.partial(self._string_func, lower=lower),
157 'dir': self._dir_func,
155 'dir': self._dir_func,
158 'dir:ensured': functools.partial(self._dir_func, ensure_dir=True),
156 'dir:ensured': functools.partial(self._dir_func, ensure_dir=True),
159 'file': self._file_path_func,
157 'file': self._file_path_func,
160 'file:ensured': functools.partial(self._file_path_func, ensure_dir=True),
158 'file:ensured': functools.partial(self._file_path_func, ensure_dir=True),
161 None: lambda i: i
159 None: lambda i: i
162 }[parser]
160 }[parser]
163
161
164 envvar_value = self.maybe_env_key(key)
162 envvar_value = self.maybe_env_key(key)
165 if envvar_value:
163 if envvar_value:
166 input_val = envvar_value
164 input_val = envvar_value
167 set_keys[key] = input_val
165 set_keys[key] = input_val
168
166
169 self.settings[key] = parser_func(input_val)
167 self.settings[key] = parser_func(input_val)
170 return self.settings[key]
168 return self.settings[key]
@@ -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 from pyramid.config import Configurator
22 from pyramid.config import Configurator
23 from pyramid.response import Response, FileIter
23 from pyramid.response import Response, FileIter
24 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
25 HTTPBadRequest, HTTPNotImplemented, HTTPNotFound, HTTPForbidden,
25 HTTPBadRequest, HTTPNotImplemented, HTTPNotFound, HTTPForbidden,
26 HTTPUnprocessableEntity)
26 HTTPUnprocessableEntity)
27
27
28 from vcsserver.lib.rc_json import json
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.str_utils import safe_int
31 from vcsserver.str_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.body = 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,204 +1,202 b''
1 # -*- coding: utf-8 -*-
2
3 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
5 #
3 #
6 # 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
7 # 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
8 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
7 # (at your option) any later version.
10 #
8 #
11 # 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,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
12 # GNU General Public License for more details.
15 #
13 #
16 # 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
17 # 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,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
17
20 import re
18 import re
21 import os
19 import os
22 import sys
20 import sys
23 import datetime
21 import datetime
24 import logging
22 import logging
25 import pkg_resources
23 import pkg_resources
26
24
27 import vcsserver
25 import vcsserver
28 from vcsserver.str_utils import safe_bytes
26 from vcsserver.str_utils import safe_bytes
29
27
30 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
31
29
32
30
33 def get_git_hooks_path(repo_path, bare):
31 def get_git_hooks_path(repo_path, bare):
34 hooks_path = os.path.join(repo_path, 'hooks')
32 hooks_path = os.path.join(repo_path, 'hooks')
35 if not bare:
33 if not bare:
36 hooks_path = os.path.join(repo_path, '.git', 'hooks')
34 hooks_path = os.path.join(repo_path, '.git', 'hooks')
37
35
38 return hooks_path
36 return hooks_path
39
37
40
38
41 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
39 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
42 """
40 """
43 Creates a RhodeCode hook inside a git repository
41 Creates a RhodeCode hook inside a git repository
44
42
45 :param repo_path: path to repository
43 :param repo_path: path to repository
46 :param executable: binary executable to put in the hooks
44 :param executable: binary executable to put in the hooks
47 :param force_create: Create even if same name hook exists
45 :param force_create: Create even if same name hook exists
48 """
46 """
49 executable = executable or sys.executable
47 executable = executable or sys.executable
50 hooks_path = get_git_hooks_path(repo_path, bare)
48 hooks_path = get_git_hooks_path(repo_path, bare)
51
49
52 if not os.path.isdir(hooks_path):
50 if not os.path.isdir(hooks_path):
53 os.makedirs(hooks_path, mode=0o777)
51 os.makedirs(hooks_path, mode=0o777)
54
52
55 tmpl_post = pkg_resources.resource_string(
53 tmpl_post = pkg_resources.resource_string(
56 'vcsserver', '/'.join(
54 'vcsserver', '/'.join(
57 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
55 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
58 tmpl_pre = pkg_resources.resource_string(
56 tmpl_pre = pkg_resources.resource_string(
59 'vcsserver', '/'.join(
57 'vcsserver', '/'.join(
60 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
58 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
61
59
62 path = '' # not used for now
60 path = '' # not used for now
63 timestamp = datetime.datetime.utcnow().isoformat()
61 timestamp = datetime.datetime.utcnow().isoformat()
64
62
65 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
63 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
66 log.debug('Installing git hook in repo %s', repo_path)
64 log.debug('Installing git hook in repo %s', repo_path)
67 _hook_file = os.path.join(hooks_path, '%s-receive' % h_type)
65 _hook_file = os.path.join(hooks_path, '%s-receive' % h_type)
68 _rhodecode_hook = check_rhodecode_hook(_hook_file)
66 _rhodecode_hook = check_rhodecode_hook(_hook_file)
69
67
70 if _rhodecode_hook or force_create:
68 if _rhodecode_hook or force_create:
71 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
69 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
72 try:
70 try:
73 with open(_hook_file, 'wb') as f:
71 with open(_hook_file, 'wb') as f:
74 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.__version__))
72 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.__version__))
75 template = template.replace(b'_DATE_', safe_bytes(timestamp))
73 template = template.replace(b'_DATE_', safe_bytes(timestamp))
76 template = template.replace(b'_ENV_', safe_bytes(executable))
74 template = template.replace(b'_ENV_', safe_bytes(executable))
77 template = template.replace(b'_PATH_', safe_bytes(path))
75 template = template.replace(b'_PATH_', safe_bytes(path))
78 f.write(template)
76 f.write(template)
79 os.chmod(_hook_file, 0o755)
77 os.chmod(_hook_file, 0o755)
80 except IOError:
78 except IOError:
81 log.exception('error writing hook file %s', _hook_file)
79 log.exception('error writing hook file %s', _hook_file)
82 else:
80 else:
83 log.debug('skipping writing hook file')
81 log.debug('skipping writing hook file')
84
82
85 return True
83 return True
86
84
87
85
88 def get_svn_hooks_path(repo_path):
86 def get_svn_hooks_path(repo_path):
89 hooks_path = os.path.join(repo_path, 'hooks')
87 hooks_path = os.path.join(repo_path, 'hooks')
90
88
91 return hooks_path
89 return hooks_path
92
90
93
91
94 def install_svn_hooks(repo_path, executable=None, force_create=False):
92 def install_svn_hooks(repo_path, executable=None, force_create=False):
95 """
93 """
96 Creates RhodeCode hooks inside a svn repository
94 Creates RhodeCode hooks inside a svn repository
97
95
98 :param repo_path: path to repository
96 :param repo_path: path to repository
99 :param executable: binary executable to put in the hooks
97 :param executable: binary executable to put in the hooks
100 :param force_create: Create even if same name hook exists
98 :param force_create: Create even if same name hook exists
101 """
99 """
102 executable = executable or sys.executable
100 executable = executable or sys.executable
103 hooks_path = get_svn_hooks_path(repo_path)
101 hooks_path = get_svn_hooks_path(repo_path)
104 if not os.path.isdir(hooks_path):
102 if not os.path.isdir(hooks_path):
105 os.makedirs(hooks_path, mode=0o777)
103 os.makedirs(hooks_path, mode=0o777)
106
104
107 tmpl_post = pkg_resources.resource_string(
105 tmpl_post = pkg_resources.resource_string(
108 'vcsserver', '/'.join(
106 'vcsserver', '/'.join(
109 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
107 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
110 tmpl_pre = pkg_resources.resource_string(
108 tmpl_pre = pkg_resources.resource_string(
111 'vcsserver', '/'.join(
109 'vcsserver', '/'.join(
112 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
110 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
113
111
114 path = '' # not used for now
112 path = '' # not used for now
115 timestamp = datetime.datetime.utcnow().isoformat()
113 timestamp = datetime.datetime.utcnow().isoformat()
116
114
117 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
115 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
118 log.debug('Installing svn hook in repo %s', repo_path)
116 log.debug('Installing svn hook in repo %s', repo_path)
119 _hook_file = os.path.join(hooks_path, '%s-commit' % h_type)
117 _hook_file = os.path.join(hooks_path, '%s-commit' % h_type)
120 _rhodecode_hook = check_rhodecode_hook(_hook_file)
118 _rhodecode_hook = check_rhodecode_hook(_hook_file)
121
119
122 if _rhodecode_hook or force_create:
120 if _rhodecode_hook or force_create:
123 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
121 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
124
122
125 try:
123 try:
126 with open(_hook_file, 'wb') as f:
124 with open(_hook_file, 'wb') as f:
127 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.__version__))
125 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.__version__))
128 template = template.replace(b'_DATE_', safe_bytes(timestamp))
126 template = template.replace(b'_DATE_', safe_bytes(timestamp))
129 template = template.replace(b'_ENV_', safe_bytes(executable))
127 template = template.replace(b'_ENV_', safe_bytes(executable))
130 template = template.replace(b'_PATH_', safe_bytes(path))
128 template = template.replace(b'_PATH_', safe_bytes(path))
131
129
132 f.write(template)
130 f.write(template)
133 os.chmod(_hook_file, 0o755)
131 os.chmod(_hook_file, 0o755)
134 except IOError:
132 except IOError:
135 log.exception('error writing hook file %s', _hook_file)
133 log.exception('error writing hook file %s', _hook_file)
136 else:
134 else:
137 log.debug('skipping writing hook file')
135 log.debug('skipping writing hook file')
138
136
139 return True
137 return True
140
138
141
139
142 def get_version_from_hook(hook_path):
140 def get_version_from_hook(hook_path):
143 version = b''
141 version = b''
144 hook_content = read_hook_content(hook_path)
142 hook_content = read_hook_content(hook_path)
145 matches = re.search(rb'(?:RC_HOOK_VER)\s*=\s*(.*)', hook_content)
143 matches = re.search(rb'(?:RC_HOOK_VER)\s*=\s*(.*)', hook_content)
146 if matches:
144 if matches:
147 try:
145 try:
148 version = matches.groups()[0]
146 version = matches.groups()[0]
149 log.debug('got version %s from hooks.', version)
147 log.debug('got version %s from hooks.', version)
150 except Exception:
148 except Exception:
151 log.exception("Exception while reading the hook version.")
149 log.exception("Exception while reading the hook version.")
152 return version.replace(b"'", b"")
150 return version.replace(b"'", b"")
153
151
154
152
155 def check_rhodecode_hook(hook_path):
153 def check_rhodecode_hook(hook_path):
156 """
154 """
157 Check if the hook was created by RhodeCode
155 Check if the hook was created by RhodeCode
158 """
156 """
159 if not os.path.exists(hook_path):
157 if not os.path.exists(hook_path):
160 return True
158 return True
161
159
162 log.debug('hook exists, checking if it is from RhodeCode')
160 log.debug('hook exists, checking if it is from RhodeCode')
163
161
164 version = get_version_from_hook(hook_path)
162 version = get_version_from_hook(hook_path)
165 if version:
163 if version:
166 return True
164 return True
167
165
168 return False
166 return False
169
167
170
168
171 def read_hook_content(hook_path) -> bytes:
169 def read_hook_content(hook_path) -> bytes:
172 content = b''
170 content = b''
173 if os.path.isfile(hook_path):
171 if os.path.isfile(hook_path):
174 with open(hook_path, 'rb') as f:
172 with open(hook_path, 'rb') as f:
175 content = f.read()
173 content = f.read()
176 return content
174 return content
177
175
178
176
179 def get_git_pre_hook_version(repo_path, bare):
177 def get_git_pre_hook_version(repo_path, bare):
180 hooks_path = get_git_hooks_path(repo_path, bare)
178 hooks_path = get_git_hooks_path(repo_path, bare)
181 _hook_file = os.path.join(hooks_path, 'pre-receive')
179 _hook_file = os.path.join(hooks_path, 'pre-receive')
182 version = get_version_from_hook(_hook_file)
180 version = get_version_from_hook(_hook_file)
183 return version
181 return version
184
182
185
183
186 def get_git_post_hook_version(repo_path, bare):
184 def get_git_post_hook_version(repo_path, bare):
187 hooks_path = get_git_hooks_path(repo_path, bare)
185 hooks_path = get_git_hooks_path(repo_path, bare)
188 _hook_file = os.path.join(hooks_path, 'post-receive')
186 _hook_file = os.path.join(hooks_path, 'post-receive')
189 version = get_version_from_hook(_hook_file)
187 version = get_version_from_hook(_hook_file)
190 return version
188 return version
191
189
192
190
193 def get_svn_pre_hook_version(repo_path):
191 def get_svn_pre_hook_version(repo_path):
194 hooks_path = get_svn_hooks_path(repo_path)
192 hooks_path = get_svn_hooks_path(repo_path)
195 _hook_file = os.path.join(hooks_path, 'pre-commit')
193 _hook_file = os.path.join(hooks_path, 'pre-commit')
196 version = get_version_from_hook(_hook_file)
194 version = get_version_from_hook(_hook_file)
197 return version
195 return version
198
196
199
197
200 def get_svn_post_hook_version(repo_path):
198 def get_svn_post_hook_version(repo_path):
201 hooks_path = get_svn_hooks_path(repo_path)
199 hooks_path = get_svn_hooks_path(repo_path)
202 _hook_file = os.path.join(hooks_path, 'post-commit')
200 _hook_file = os.path.join(hooks_path, 'post-commit')
203 version = get_version_from_hook(_hook_file)
201 version = get_version_from_hook(_hook_file)
204 return version
202 return version
@@ -1,777 +1,779 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 import logging
21 import logging
22 import collections
22 import collections
23 import importlib
23 import importlib
24 import base64
24 import base64
25 import msgpack
25 import msgpack
26 import dataclasses
26 import dataclasses
27 import pygit2
27 import pygit2
28
28
29 import http.client
29 import http.client
30
30
31
31
32 import mercurial.scmutil
32 import mercurial.scmutil
33 import mercurial.node
33 import mercurial.node
34
34
35 from vcsserver.lib.rc_json import json
35 from vcsserver.lib.rc_json import json
36 from vcsserver import exceptions, subprocessio, settings
36 from vcsserver import exceptions, subprocessio, settings
37 from vcsserver.str_utils import ascii_str, safe_str
37 from vcsserver.str_utils import ascii_str, safe_str
38 from vcsserver.remote.git import Repository
38 from vcsserver.remote.git import Repository
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class HooksHttpClient(object):
43 class HooksHttpClient(object):
44 proto = 'msgpack.v1'
44 proto = 'msgpack.v1'
45 connection = None
45 connection = None
46
46
47 def __init__(self, hooks_uri):
47 def __init__(self, hooks_uri):
48 self.hooks_uri = hooks_uri
48 self.hooks_uri = hooks_uri
49
49
50 def __repr__(self):
50 def __repr__(self):
51 return f'{self.__class__}(hook_uri={self.hooks_uri}, proto={self.proto})'
51 return f'{self.__class__}(hook_uri={self.hooks_uri}, proto={self.proto})'
52
52
53 def __call__(self, method, extras):
53 def __call__(self, method, extras):
54 connection = http.client.HTTPConnection(self.hooks_uri)
54 connection = http.client.HTTPConnection(self.hooks_uri)
55 # binary msgpack body
55 # binary msgpack body
56 headers, body = self._serialize(method, extras)
56 headers, body = self._serialize(method, extras)
57 log.debug('Doing a new hooks call using HTTPConnection to %s', self.hooks_uri)
57 log.debug('Doing a new hooks call using HTTPConnection to %s', self.hooks_uri)
58
58
59 try:
59 try:
60 try:
60 try:
61 connection.request('POST', '/', body, headers)
61 connection.request('POST', '/', body, headers)
62 except Exception as error:
62 except Exception as error:
63 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
63 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
64 raise
64 raise
65
65
66 response = connection.getresponse()
66 response = connection.getresponse()
67 try:
67 try:
68 return msgpack.load(response)
68 return msgpack.load(response)
69 except Exception:
69 except Exception:
70 response_data = response.read()
70 response_data = response.read()
71 log.exception('Failed to decode hook response json data. '
71 log.exception('Failed to decode hook response json data. '
72 'response_code:%s, raw_data:%s',
72 'response_code:%s, raw_data:%s',
73 response.status, response_data)
73 response.status, response_data)
74 raise
74 raise
75 finally:
75 finally:
76 connection.close()
76 connection.close()
77
77
78 @classmethod
78 @classmethod
79 def _serialize(cls, hook_name, extras):
79 def _serialize(cls, hook_name, extras):
80 data = {
80 data = {
81 'method': hook_name,
81 'method': hook_name,
82 'extras': extras
82 'extras': extras
83 }
83 }
84 headers = {
84 headers = {
85 "rc-hooks-protocol": cls.proto,
85 "rc-hooks-protocol": cls.proto,
86 "Connection": "keep-alive"
86 "Connection": "keep-alive"
87 }
87 }
88 return headers, msgpack.packb(data)
88 return headers, msgpack.packb(data)
89
89
90
90
91 class HooksDummyClient(object):
91 class HooksDummyClient(object):
92 def __init__(self, hooks_module):
92 def __init__(self, hooks_module):
93 self._hooks_module = importlib.import_module(hooks_module)
93 self._hooks_module = importlib.import_module(hooks_module)
94
94
95 def __call__(self, hook_name, extras):
95 def __call__(self, hook_name, extras):
96 with self._hooks_module.Hooks() as hooks:
96 with self._hooks_module.Hooks() as hooks:
97 return getattr(hooks, hook_name)(extras)
97 return getattr(hooks, hook_name)(extras)
98
98
99
99
100 class HooksShadowRepoClient(object):
100 class HooksShadowRepoClient(object):
101
101
102 def __call__(self, hook_name, extras):
102 def __call__(self, hook_name, extras):
103 return {'output': '', 'status': 0}
103 return {'output': '', 'status': 0}
104
104
105
105
106 class RemoteMessageWriter(object):
106 class RemoteMessageWriter(object):
107 """Writer base class."""
107 """Writer base class."""
108 def write(self, message):
108 def write(self, message):
109 raise NotImplementedError()
109 raise NotImplementedError()
110
110
111
111
112 class HgMessageWriter(RemoteMessageWriter):
112 class HgMessageWriter(RemoteMessageWriter):
113 """Writer that knows how to send messages to mercurial clients."""
113 """Writer that knows how to send messages to mercurial clients."""
114
114
115 def __init__(self, ui):
115 def __init__(self, ui):
116 self.ui = ui
116 self.ui = ui
117
117
118 def write(self, message: str):
118 def write(self, message: str):
119 # TODO: Check why the quiet flag is set by default.
119 # TODO: Check why the quiet flag is set by default.
120 old = self.ui.quiet
120 old = self.ui.quiet
121 self.ui.quiet = False
121 self.ui.quiet = False
122 self.ui.status(message.encode('utf-8'))
122 self.ui.status(message.encode('utf-8'))
123 self.ui.quiet = old
123 self.ui.quiet = old
124
124
125
125
126 class GitMessageWriter(RemoteMessageWriter):
126 class GitMessageWriter(RemoteMessageWriter):
127 """Writer that knows how to send messages to git clients."""
127 """Writer that knows how to send messages to git clients."""
128
128
129 def __init__(self, stdout=None):
129 def __init__(self, stdout=None):
130 self.stdout = stdout or sys.stdout
130 self.stdout = stdout or sys.stdout
131
131
132 def write(self, message: str):
132 def write(self, message: str):
133 self.stdout.write(message)
133 self.stdout.write(message)
134
134
135
135
136 class SvnMessageWriter(RemoteMessageWriter):
136 class SvnMessageWriter(RemoteMessageWriter):
137 """Writer that knows how to send messages to svn clients."""
137 """Writer that knows how to send messages to svn clients."""
138
138
139 def __init__(self, stderr=None):
139 def __init__(self, stderr=None):
140 # SVN needs data sent to stderr for back-to-client messaging
140 # SVN needs data sent to stderr for back-to-client messaging
141 self.stderr = stderr or sys.stderr
141 self.stderr = stderr or sys.stderr
142
142
143 def write(self, message):
143 def write(self, message):
144 self.stderr.write(message.encode('utf-8'))
144 self.stderr.write(message.encode('utf-8'))
145
145
146
146
147 def _handle_exception(result):
147 def _handle_exception(result):
148 exception_class = result.get('exception')
148 exception_class = result.get('exception')
149 exception_traceback = result.get('exception_traceback')
149 exception_traceback = result.get('exception_traceback')
150 log.debug('Handling hook-call exception: %s', exception_class)
150
151
151 if exception_traceback:
152 if exception_traceback:
152 log.error('Got traceback from remote call:%s', exception_traceback)
153 log.error('Got traceback from remote call:%s', exception_traceback)
153
154
154 if exception_class == 'HTTPLockedRC':
155 if exception_class == 'HTTPLockedRC':
155 raise exceptions.RepositoryLockedException()(*result['exception_args'])
156 raise exceptions.RepositoryLockedException()(*result['exception_args'])
156 elif exception_class == 'HTTPBranchProtected':
157 elif exception_class == 'HTTPBranchProtected':
157 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
158 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
158 elif exception_class == 'RepositoryError':
159 elif exception_class == 'RepositoryError':
159 raise exceptions.VcsException()(*result['exception_args'])
160 raise exceptions.VcsException()(*result['exception_args'])
160 elif exception_class:
161 elif exception_class:
161 raise Exception(
162 raise Exception(
162 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
163 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
163 )
164 )
164
165
165
166
166 def _get_hooks_client(extras):
167 def _get_hooks_client(extras):
167 hooks_uri = extras.get('hooks_uri')
168 hooks_uri = extras.get('hooks_uri')
168 is_shadow_repo = extras.get('is_shadow_repo')
169 is_shadow_repo = extras.get('is_shadow_repo')
170
169 if hooks_uri:
171 if hooks_uri:
170 return HooksHttpClient(extras['hooks_uri'])
172 return HooksHttpClient(extras['hooks_uri'])
171 elif is_shadow_repo:
173 elif is_shadow_repo:
172 return HooksShadowRepoClient()
174 return HooksShadowRepoClient()
173 else:
175 else:
174 return HooksDummyClient(extras['hooks_module'])
176 return HooksDummyClient(extras['hooks_module'])
175
177
176
178
177 def _call_hook(hook_name, extras, writer):
179 def _call_hook(hook_name, extras, writer):
178 hooks_client = _get_hooks_client(extras)
180 hooks_client = _get_hooks_client(extras)
179 log.debug('Hooks, using client:%s', hooks_client)
181 log.debug('Hooks, using client:%s', hooks_client)
180 result = hooks_client(hook_name, extras)
182 result = hooks_client(hook_name, extras)
181 log.debug('Hooks got result: %s', result)
183 log.debug('Hooks got result: %s', result)
182 _handle_exception(result)
184 _handle_exception(result)
183 writer.write(result['output'])
185 writer.write(result['output'])
184
186
185 return result['status']
187 return result['status']
186
188
187
189
188 def _extras_from_ui(ui):
190 def _extras_from_ui(ui):
189 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
191 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
190 if not hook_data:
192 if not hook_data:
191 # maybe it's inside environ ?
193 # maybe it's inside environ ?
192 env_hook_data = os.environ.get('RC_SCM_DATA')
194 env_hook_data = os.environ.get('RC_SCM_DATA')
193 if env_hook_data:
195 if env_hook_data:
194 hook_data = env_hook_data
196 hook_data = env_hook_data
195
197
196 extras = {}
198 extras = {}
197 if hook_data:
199 if hook_data:
198 extras = json.loads(hook_data)
200 extras = json.loads(hook_data)
199 return extras
201 return extras
200
202
201
203
202 def _rev_range_hash(repo, node, check_heads=False):
204 def _rev_range_hash(repo, node, check_heads=False):
203 from vcsserver.hgcompat import get_ctx
205 from vcsserver.hgcompat import get_ctx
204
206
205 commits = []
207 commits = []
206 revs = []
208 revs = []
207 start = get_ctx(repo, node).rev()
209 start = get_ctx(repo, node).rev()
208 end = len(repo)
210 end = len(repo)
209 for rev in range(start, end):
211 for rev in range(start, end):
210 revs.append(rev)
212 revs.append(rev)
211 ctx = get_ctx(repo, rev)
213 ctx = get_ctx(repo, rev)
212 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
214 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
213 branch = safe_str(ctx.branch())
215 branch = safe_str(ctx.branch())
214 commits.append((commit_id, branch))
216 commits.append((commit_id, branch))
215
217
216 parent_heads = []
218 parent_heads = []
217 if check_heads:
219 if check_heads:
218 parent_heads = _check_heads(repo, start, end, revs)
220 parent_heads = _check_heads(repo, start, end, revs)
219 return commits, parent_heads
221 return commits, parent_heads
220
222
221
223
222 def _check_heads(repo, start, end, commits):
224 def _check_heads(repo, start, end, commits):
223 from vcsserver.hgcompat import get_ctx
225 from vcsserver.hgcompat import get_ctx
224 changelog = repo.changelog
226 changelog = repo.changelog
225 parents = set()
227 parents = set()
226
228
227 for new_rev in commits:
229 for new_rev in commits:
228 for p in changelog.parentrevs(new_rev):
230 for p in changelog.parentrevs(new_rev):
229 if p == mercurial.node.nullrev:
231 if p == mercurial.node.nullrev:
230 continue
232 continue
231 if p < start:
233 if p < start:
232 parents.add(p)
234 parents.add(p)
233
235
234 for p in parents:
236 for p in parents:
235 branch = get_ctx(repo, p).branch()
237 branch = get_ctx(repo, p).branch()
236 # The heads descending from that parent, on the same branch
238 # The heads descending from that parent, on the same branch
237 parent_heads = {p}
239 parent_heads = {p}
238 reachable = {p}
240 reachable = {p}
239 for x in range(p + 1, end):
241 for x in range(p + 1, end):
240 if get_ctx(repo, x).branch() != branch:
242 if get_ctx(repo, x).branch() != branch:
241 continue
243 continue
242 for pp in changelog.parentrevs(x):
244 for pp in changelog.parentrevs(x):
243 if pp in reachable:
245 if pp in reachable:
244 reachable.add(x)
246 reachable.add(x)
245 parent_heads.discard(pp)
247 parent_heads.discard(pp)
246 parent_heads.add(x)
248 parent_heads.add(x)
247 # More than one head? Suggest merging
249 # More than one head? Suggest merging
248 if len(parent_heads) > 1:
250 if len(parent_heads) > 1:
249 return list(parent_heads)
251 return list(parent_heads)
250
252
251 return []
253 return []
252
254
253
255
254 def _get_git_env():
256 def _get_git_env():
255 env = {}
257 env = {}
256 for k, v in os.environ.items():
258 for k, v in os.environ.items():
257 if k.startswith('GIT'):
259 if k.startswith('GIT'):
258 env[k] = v
260 env[k] = v
259
261
260 # serialized version
262 # serialized version
261 return [(k, v) for k, v in env.items()]
263 return [(k, v) for k, v in env.items()]
262
264
263
265
264 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
266 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
265 env = {}
267 env = {}
266 for k, v in os.environ.items():
268 for k, v in os.environ.items():
267 if k.startswith('HG'):
269 if k.startswith('HG'):
268 env[k] = v
270 env[k] = v
269
271
270 env['HG_NODE'] = old_rev
272 env['HG_NODE'] = old_rev
271 env['HG_NODE_LAST'] = new_rev
273 env['HG_NODE_LAST'] = new_rev
272 env['HG_TXNID'] = txnid
274 env['HG_TXNID'] = txnid
273 env['HG_PENDING'] = repo_path
275 env['HG_PENDING'] = repo_path
274
276
275 return [(k, v) for k, v in env.items()]
277 return [(k, v) for k, v in env.items()]
276
278
277
279
278 def repo_size(ui, repo, **kwargs):
280 def repo_size(ui, repo, **kwargs):
279 extras = _extras_from_ui(ui)
281 extras = _extras_from_ui(ui)
280 return _call_hook('repo_size', extras, HgMessageWriter(ui))
282 return _call_hook('repo_size', extras, HgMessageWriter(ui))
281
283
282
284
283 def pre_pull(ui, repo, **kwargs):
285 def pre_pull(ui, repo, **kwargs):
284 extras = _extras_from_ui(ui)
286 extras = _extras_from_ui(ui)
285 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
287 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
286
288
287
289
288 def pre_pull_ssh(ui, repo, **kwargs):
290 def pre_pull_ssh(ui, repo, **kwargs):
289 extras = _extras_from_ui(ui)
291 extras = _extras_from_ui(ui)
290 if extras and extras.get('SSH'):
292 if extras and extras.get('SSH'):
291 return pre_pull(ui, repo, **kwargs)
293 return pre_pull(ui, repo, **kwargs)
292 return 0
294 return 0
293
295
294
296
295 def post_pull(ui, repo, **kwargs):
297 def post_pull(ui, repo, **kwargs):
296 extras = _extras_from_ui(ui)
298 extras = _extras_from_ui(ui)
297 return _call_hook('post_pull', extras, HgMessageWriter(ui))
299 return _call_hook('post_pull', extras, HgMessageWriter(ui))
298
300
299
301
300 def post_pull_ssh(ui, repo, **kwargs):
302 def post_pull_ssh(ui, repo, **kwargs):
301 extras = _extras_from_ui(ui)
303 extras = _extras_from_ui(ui)
302 if extras and extras.get('SSH'):
304 if extras and extras.get('SSH'):
303 return post_pull(ui, repo, **kwargs)
305 return post_pull(ui, repo, **kwargs)
304 return 0
306 return 0
305
307
306
308
307 def pre_push(ui, repo, node=None, **kwargs):
309 def pre_push(ui, repo, node=None, **kwargs):
308 """
310 """
309 Mercurial pre_push hook
311 Mercurial pre_push hook
310 """
312 """
311 extras = _extras_from_ui(ui)
313 extras = _extras_from_ui(ui)
312 detect_force_push = extras.get('detect_force_push')
314 detect_force_push = extras.get('detect_force_push')
313
315
314 rev_data = []
316 rev_data = []
315 hook_type: str = safe_str(kwargs.get('hooktype'))
317 hook_type: str = safe_str(kwargs.get('hooktype'))
316
318
317 if node and hook_type == 'pretxnchangegroup':
319 if node and hook_type == 'pretxnchangegroup':
318 branches = collections.defaultdict(list)
320 branches = collections.defaultdict(list)
319 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
321 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
320 for commit_id, branch in commits:
322 for commit_id, branch in commits:
321 branches[branch].append(commit_id)
323 branches[branch].append(commit_id)
322
324
323 for branch, commits in branches.items():
325 for branch, commits in branches.items():
324 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
326 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
325 rev_data.append({
327 rev_data.append({
326 'total_commits': len(commits),
328 'total_commits': len(commits),
327 'old_rev': old_rev,
329 'old_rev': old_rev,
328 'new_rev': commits[-1],
330 'new_rev': commits[-1],
329 'ref': '',
331 'ref': '',
330 'type': 'branch',
332 'type': 'branch',
331 'name': branch,
333 'name': branch,
332 })
334 })
333
335
334 for push_ref in rev_data:
336 for push_ref in rev_data:
335 push_ref['multiple_heads'] = _heads
337 push_ref['multiple_heads'] = _heads
336
338
337 repo_path = os.path.join(
339 repo_path = os.path.join(
338 extras.get('repo_store', ''), extras.get('repository', ''))
340 extras.get('repo_store', ''), extras.get('repository', ''))
339 push_ref['hg_env'] = _get_hg_env(
341 push_ref['hg_env'] = _get_hg_env(
340 old_rev=push_ref['old_rev'],
342 old_rev=push_ref['old_rev'],
341 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
343 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
342 repo_path=repo_path)
344 repo_path=repo_path)
343
345
344 extras['hook_type'] = hook_type or 'pre_push'
346 extras['hook_type'] = hook_type or 'pre_push'
345 extras['commit_ids'] = rev_data
347 extras['commit_ids'] = rev_data
346
348
347 return _call_hook('pre_push', extras, HgMessageWriter(ui))
349 return _call_hook('pre_push', extras, HgMessageWriter(ui))
348
350
349
351
350 def pre_push_ssh(ui, repo, node=None, **kwargs):
352 def pre_push_ssh(ui, repo, node=None, **kwargs):
351 extras = _extras_from_ui(ui)
353 extras = _extras_from_ui(ui)
352 if extras.get('SSH'):
354 if extras.get('SSH'):
353 return pre_push(ui, repo, node, **kwargs)
355 return pre_push(ui, repo, node, **kwargs)
354
356
355 return 0
357 return 0
356
358
357
359
358 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
360 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
359 """
361 """
360 Mercurial pre_push hook for SSH
362 Mercurial pre_push hook for SSH
361 """
363 """
362 extras = _extras_from_ui(ui)
364 extras = _extras_from_ui(ui)
363 if extras.get('SSH'):
365 if extras.get('SSH'):
364 permission = extras['SSH_PERMISSIONS']
366 permission = extras['SSH_PERMISSIONS']
365
367
366 if 'repository.write' == permission or 'repository.admin' == permission:
368 if 'repository.write' == permission or 'repository.admin' == permission:
367 return 0
369 return 0
368
370
369 # non-zero ret code
371 # non-zero ret code
370 return 1
372 return 1
371
373
372 return 0
374 return 0
373
375
374
376
375 def post_push(ui, repo, node, **kwargs):
377 def post_push(ui, repo, node, **kwargs):
376 """
378 """
377 Mercurial post_push hook
379 Mercurial post_push hook
378 """
380 """
379 extras = _extras_from_ui(ui)
381 extras = _extras_from_ui(ui)
380
382
381 commit_ids = []
383 commit_ids = []
382 branches = []
384 branches = []
383 bookmarks = []
385 bookmarks = []
384 tags = []
386 tags = []
385 hook_type: str = safe_str(kwargs.get('hooktype'))
387 hook_type: str = safe_str(kwargs.get('hooktype'))
386
388
387 commits, _heads = _rev_range_hash(repo, node)
389 commits, _heads = _rev_range_hash(repo, node)
388 for commit_id, branch in commits:
390 for commit_id, branch in commits:
389 commit_ids.append(commit_id)
391 commit_ids.append(commit_id)
390 if branch not in branches:
392 if branch not in branches:
391 branches.append(branch)
393 branches.append(branch)
392
394
393 if hasattr(ui, '_rc_pushkey_bookmarks'):
395 if hasattr(ui, '_rc_pushkey_bookmarks'):
394 bookmarks = ui._rc_pushkey_bookmarks
396 bookmarks = ui._rc_pushkey_bookmarks
395
397
396 extras['hook_type'] = hook_type or 'post_push'
398 extras['hook_type'] = hook_type or 'post_push'
397 extras['commit_ids'] = commit_ids
399 extras['commit_ids'] = commit_ids
398
400
399 extras['new_refs'] = {
401 extras['new_refs'] = {
400 'branches': branches,
402 'branches': branches,
401 'bookmarks': bookmarks,
403 'bookmarks': bookmarks,
402 'tags': tags
404 'tags': tags
403 }
405 }
404
406
405 return _call_hook('post_push', extras, HgMessageWriter(ui))
407 return _call_hook('post_push', extras, HgMessageWriter(ui))
406
408
407
409
408 def post_push_ssh(ui, repo, node, **kwargs):
410 def post_push_ssh(ui, repo, node, **kwargs):
409 """
411 """
410 Mercurial post_push hook for SSH
412 Mercurial post_push hook for SSH
411 """
413 """
412 if _extras_from_ui(ui).get('SSH'):
414 if _extras_from_ui(ui).get('SSH'):
413 return post_push(ui, repo, node, **kwargs)
415 return post_push(ui, repo, node, **kwargs)
414 return 0
416 return 0
415
417
416
418
417 def key_push(ui, repo, **kwargs):
419 def key_push(ui, repo, **kwargs):
418 from vcsserver.hgcompat import get_ctx
420 from vcsserver.hgcompat import get_ctx
419
421
420 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
422 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
421 # store new bookmarks in our UI object propagated later to post_push
423 # store new bookmarks in our UI object propagated later to post_push
422 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
424 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
423 return
425 return
424
426
425
427
426 # backward compat
428 # backward compat
427 log_pull_action = post_pull
429 log_pull_action = post_pull
428
430
429 # backward compat
431 # backward compat
430 log_push_action = post_push
432 log_push_action = post_push
431
433
432
434
433 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
435 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
434 """
436 """
435 Old hook name: keep here for backward compatibility.
437 Old hook name: keep here for backward compatibility.
436
438
437 This is only required when the installed git hooks are not upgraded.
439 This is only required when the installed git hooks are not upgraded.
438 """
440 """
439 pass
441 pass
440
442
441
443
442 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
444 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
443 """
445 """
444 Old hook name: keep here for backward compatibility.
446 Old hook name: keep here for backward compatibility.
445
447
446 This is only required when the installed git hooks are not upgraded.
448 This is only required when the installed git hooks are not upgraded.
447 """
449 """
448 pass
450 pass
449
451
450
452
451 @dataclasses.dataclass
453 @dataclasses.dataclass
452 class HookResponse:
454 class HookResponse:
453 status: int
455 status: int
454 output: str
456 output: str
455
457
456
458
457 def git_pre_pull(extras) -> HookResponse:
459 def git_pre_pull(extras) -> HookResponse:
458 """
460 """
459 Pre pull hook.
461 Pre pull hook.
460
462
461 :param extras: dictionary containing the keys defined in simplevcs
463 :param extras: dictionary containing the keys defined in simplevcs
462 :type extras: dict
464 :type extras: dict
463
465
464 :return: status code of the hook. 0 for success.
466 :return: status code of the hook. 0 for success.
465 :rtype: int
467 :rtype: int
466 """
468 """
467
469
468 if 'pull' not in extras['hooks']:
470 if 'pull' not in extras['hooks']:
469 return HookResponse(0, '')
471 return HookResponse(0, '')
470
472
471 stdout = io.StringIO()
473 stdout = io.StringIO()
472 try:
474 try:
473 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
475 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
474
476
475 except Exception as error:
477 except Exception as error:
476 log.exception('Failed to call pre_pull hook')
478 log.exception('Failed to call pre_pull hook')
477 status_code = 128
479 status_code = 128
478 stdout.write(f'ERROR: {error}\n')
480 stdout.write(f'ERROR: {error}\n')
479
481
480 return HookResponse(status_code, stdout.getvalue())
482 return HookResponse(status_code, stdout.getvalue())
481
483
482
484
483 def git_post_pull(extras) -> HookResponse:
485 def git_post_pull(extras) -> HookResponse:
484 """
486 """
485 Post pull hook.
487 Post pull hook.
486
488
487 :param extras: dictionary containing the keys defined in simplevcs
489 :param extras: dictionary containing the keys defined in simplevcs
488 :type extras: dict
490 :type extras: dict
489
491
490 :return: status code of the hook. 0 for success.
492 :return: status code of the hook. 0 for success.
491 :rtype: int
493 :rtype: int
492 """
494 """
493 if 'pull' not in extras['hooks']:
495 if 'pull' not in extras['hooks']:
494 return HookResponse(0, '')
496 return HookResponse(0, '')
495
497
496 stdout = io.StringIO()
498 stdout = io.StringIO()
497 try:
499 try:
498 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
500 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
499 except Exception as error:
501 except Exception as error:
500 status = 128
502 status = 128
501 stdout.write(f'ERROR: {error}\n')
503 stdout.write(f'ERROR: {error}\n')
502
504
503 return HookResponse(status, stdout.getvalue())
505 return HookResponse(status, stdout.getvalue())
504
506
505
507
506 def _parse_git_ref_lines(revision_lines):
508 def _parse_git_ref_lines(revision_lines):
507 rev_data = []
509 rev_data = []
508 for revision_line in revision_lines or []:
510 for revision_line in revision_lines or []:
509 old_rev, new_rev, ref = revision_line.strip().split(' ')
511 old_rev, new_rev, ref = revision_line.strip().split(' ')
510 ref_data = ref.split('/', 2)
512 ref_data = ref.split('/', 2)
511 if ref_data[1] in ('tags', 'heads'):
513 if ref_data[1] in ('tags', 'heads'):
512 rev_data.append({
514 rev_data.append({
513 # NOTE(marcink):
515 # NOTE(marcink):
514 # we're unable to tell total_commits for git at this point
516 # we're unable to tell total_commits for git at this point
515 # but we set the variable for consistency with GIT
517 # but we set the variable for consistency with GIT
516 'total_commits': -1,
518 'total_commits': -1,
517 'old_rev': old_rev,
519 'old_rev': old_rev,
518 'new_rev': new_rev,
520 'new_rev': new_rev,
519 'ref': ref,
521 'ref': ref,
520 'type': ref_data[1],
522 'type': ref_data[1],
521 'name': ref_data[2],
523 'name': ref_data[2],
522 })
524 })
523 return rev_data
525 return rev_data
524
526
525
527
526 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
528 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
527 """
529 """
528 Pre push hook.
530 Pre push hook.
529
531
530 :return: status code of the hook. 0 for success.
532 :return: status code of the hook. 0 for success.
531 """
533 """
532 extras = json.loads(env['RC_SCM_DATA'])
534 extras = json.loads(env['RC_SCM_DATA'])
533 rev_data = _parse_git_ref_lines(revision_lines)
535 rev_data = _parse_git_ref_lines(revision_lines)
534 if 'push' not in extras['hooks']:
536 if 'push' not in extras['hooks']:
535 return 0
537 return 0
536 empty_commit_id = '0' * 40
538 empty_commit_id = '0' * 40
537
539
538 detect_force_push = extras.get('detect_force_push')
540 detect_force_push = extras.get('detect_force_push')
539
541
540 for push_ref in rev_data:
542 for push_ref in rev_data:
541 # store our git-env which holds the temp store
543 # store our git-env which holds the temp store
542 push_ref['git_env'] = _get_git_env()
544 push_ref['git_env'] = _get_git_env()
543 push_ref['pruned_sha'] = ''
545 push_ref['pruned_sha'] = ''
544 if not detect_force_push:
546 if not detect_force_push:
545 # don't check for forced-push when we don't need to
547 # don't check for forced-push when we don't need to
546 continue
548 continue
547
549
548 type_ = push_ref['type']
550 type_ = push_ref['type']
549 new_branch = push_ref['old_rev'] == empty_commit_id
551 new_branch = push_ref['old_rev'] == empty_commit_id
550 delete_branch = push_ref['new_rev'] == empty_commit_id
552 delete_branch = push_ref['new_rev'] == empty_commit_id
551 if type_ == 'heads' and not (new_branch or delete_branch):
553 if type_ == 'heads' and not (new_branch or delete_branch):
552 old_rev = push_ref['old_rev']
554 old_rev = push_ref['old_rev']
553 new_rev = push_ref['new_rev']
555 new_rev = push_ref['new_rev']
554 cmd = [settings.GIT_EXECUTABLE, 'rev-list', old_rev, f'^{new_rev}']
556 cmd = [settings.GIT_EXECUTABLE, 'rev-list', old_rev, f'^{new_rev}']
555 stdout, stderr = subprocessio.run_command(
557 stdout, stderr = subprocessio.run_command(
556 cmd, env=os.environ.copy())
558 cmd, env=os.environ.copy())
557 # means we're having some non-reachable objects, this forced push was used
559 # means we're having some non-reachable objects, this forced push was used
558 if stdout:
560 if stdout:
559 push_ref['pruned_sha'] = stdout.splitlines()
561 push_ref['pruned_sha'] = stdout.splitlines()
560
562
561 extras['hook_type'] = 'pre_receive'
563 extras['hook_type'] = 'pre_receive'
562 extras['commit_ids'] = rev_data
564 extras['commit_ids'] = rev_data
563
565
564 stdout = sys.stdout
566 stdout = sys.stdout
565 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
567 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
566
568
567 return status_code
569 return status_code
568
570
569
571
570 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
572 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
571 """
573 """
572 Post push hook.
574 Post push hook.
573
575
574 :return: status code of the hook. 0 for success.
576 :return: status code of the hook. 0 for success.
575 """
577 """
576 extras = json.loads(env['RC_SCM_DATA'])
578 extras = json.loads(env['RC_SCM_DATA'])
577 if 'push' not in extras['hooks']:
579 if 'push' not in extras['hooks']:
578 return 0
580 return 0
579
581
580 rev_data = _parse_git_ref_lines(revision_lines)
582 rev_data = _parse_git_ref_lines(revision_lines)
581
583
582 git_revs = []
584 git_revs = []
583
585
584 # N.B.(skreft): it is ok to just call git, as git before calling a
586 # N.B.(skreft): it is ok to just call git, as git before calling a
585 # subcommand sets the PATH environment variable so that it point to the
587 # subcommand sets the PATH environment variable so that it point to the
586 # correct version of the git executable.
588 # correct version of the git executable.
587 empty_commit_id = '0' * 40
589 empty_commit_id = '0' * 40
588 branches = []
590 branches = []
589 tags = []
591 tags = []
590 for push_ref in rev_data:
592 for push_ref in rev_data:
591 type_ = push_ref['type']
593 type_ = push_ref['type']
592
594
593 if type_ == 'heads':
595 if type_ == 'heads':
594 # starting new branch case
596 # starting new branch case
595 if push_ref['old_rev'] == empty_commit_id:
597 if push_ref['old_rev'] == empty_commit_id:
596 push_ref_name = push_ref['name']
598 push_ref_name = push_ref['name']
597
599
598 if push_ref_name not in branches:
600 if push_ref_name not in branches:
599 branches.append(push_ref_name)
601 branches.append(push_ref_name)
600
602
601 need_head_set = ''
603 need_head_set = ''
602 with Repository(os.getcwd()) as repo:
604 with Repository(os.getcwd()) as repo:
603 try:
605 try:
604 repo.head
606 repo.head
605 except pygit2.GitError:
607 except pygit2.GitError:
606 need_head_set = f'refs/heads/{push_ref_name}'
608 need_head_set = f'refs/heads/{push_ref_name}'
607
609
608 if need_head_set:
610 if need_head_set:
609 repo.set_head(need_head_set)
611 repo.set_head(need_head_set)
610 print(f"Setting default branch to {push_ref_name}")
612 print(f"Setting default branch to {push_ref_name}")
611
613
612 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
614 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
613 stdout, stderr = subprocessio.run_command(
615 stdout, stderr = subprocessio.run_command(
614 cmd, env=os.environ.copy())
616 cmd, env=os.environ.copy())
615 heads = safe_str(stdout)
617 heads = safe_str(stdout)
616 heads = heads.replace(push_ref['ref'], '')
618 heads = heads.replace(push_ref['ref'], '')
617 heads = ' '.join(head for head
619 heads = ' '.join(head for head
618 in heads.splitlines() if head) or '.'
620 in heads.splitlines() if head) or '.'
619 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
621 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
620 '--pretty=format:%H', '--', push_ref['new_rev'],
622 '--pretty=format:%H', '--', push_ref['new_rev'],
621 '--not', heads]
623 '--not', heads]
622 stdout, stderr = subprocessio.run_command(
624 stdout, stderr = subprocessio.run_command(
623 cmd, env=os.environ.copy())
625 cmd, env=os.environ.copy())
624 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
626 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
625
627
626 # delete branch case
628 # delete branch case
627 elif push_ref['new_rev'] == empty_commit_id:
629 elif push_ref['new_rev'] == empty_commit_id:
628 git_revs.append('delete_branch=>%s' % push_ref['name'])
630 git_revs.append('delete_branch=>%s' % push_ref['name'])
629 else:
631 else:
630 if push_ref['name'] not in branches:
632 if push_ref['name'] not in branches:
631 branches.append(push_ref['name'])
633 branches.append(push_ref['name'])
632
634
633 cmd = [settings.GIT_EXECUTABLE, 'log',
635 cmd = [settings.GIT_EXECUTABLE, 'log',
634 '{old_rev}..{new_rev}'.format(**push_ref),
636 '{old_rev}..{new_rev}'.format(**push_ref),
635 '--reverse', '--pretty=format:%H']
637 '--reverse', '--pretty=format:%H']
636 stdout, stderr = subprocessio.run_command(
638 stdout, stderr = subprocessio.run_command(
637 cmd, env=os.environ.copy())
639 cmd, env=os.environ.copy())
638 # we get bytes from stdout, we need str to be consistent
640 # we get bytes from stdout, we need str to be consistent
639 log_revs = list(map(ascii_str, stdout.splitlines()))
641 log_revs = list(map(ascii_str, stdout.splitlines()))
640 git_revs.extend(log_revs)
642 git_revs.extend(log_revs)
641
643
642 # Pure pygit2 impl. but still 2-3x slower :/
644 # Pure pygit2 impl. but still 2-3x slower :/
643 # results = []
645 # results = []
644 #
646 #
645 # with Repository(os.getcwd()) as repo:
647 # with Repository(os.getcwd()) as repo:
646 # repo_new_rev = repo[push_ref['new_rev']]
648 # repo_new_rev = repo[push_ref['new_rev']]
647 # repo_old_rev = repo[push_ref['old_rev']]
649 # repo_old_rev = repo[push_ref['old_rev']]
648 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
650 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
649 #
651 #
650 # for commit in walker:
652 # for commit in walker:
651 # if commit.id == repo_old_rev.id:
653 # if commit.id == repo_old_rev.id:
652 # break
654 # break
653 # results.append(commit.id.hex)
655 # results.append(commit.id.hex)
654 # # reverse the order, can't use GIT_SORT_REVERSE
656 # # reverse the order, can't use GIT_SORT_REVERSE
655 # log_revs = results[::-1]
657 # log_revs = results[::-1]
656
658
657 elif type_ == 'tags':
659 elif type_ == 'tags':
658 if push_ref['name'] not in tags:
660 if push_ref['name'] not in tags:
659 tags.append(push_ref['name'])
661 tags.append(push_ref['name'])
660 git_revs.append('tag=>%s' % push_ref['name'])
662 git_revs.append('tag=>%s' % push_ref['name'])
661
663
662 extras['hook_type'] = 'post_receive'
664 extras['hook_type'] = 'post_receive'
663 extras['commit_ids'] = git_revs
665 extras['commit_ids'] = git_revs
664 extras['new_refs'] = {
666 extras['new_refs'] = {
665 'branches': branches,
667 'branches': branches,
666 'bookmarks': [],
668 'bookmarks': [],
667 'tags': tags,
669 'tags': tags,
668 }
670 }
669
671
670 stdout = sys.stdout
672 stdout = sys.stdout
671
673
672 if 'repo_size' in extras['hooks']:
674 if 'repo_size' in extras['hooks']:
673 try:
675 try:
674 _call_hook('repo_size', extras, GitMessageWriter(stdout))
676 _call_hook('repo_size', extras, GitMessageWriter(stdout))
675 except Exception:
677 except Exception:
676 pass
678 pass
677
679
678 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
680 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
679 return status_code
681 return status_code
680
682
681
683
682 def _get_extras_from_txn_id(path, txn_id):
684 def _get_extras_from_txn_id(path, txn_id):
683 extras = {}
685 extras = {}
684 try:
686 try:
685 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
687 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
686 '-t', txn_id,
688 '-t', txn_id,
687 '--revprop', path, 'rc-scm-extras']
689 '--revprop', path, 'rc-scm-extras']
688 stdout, stderr = subprocessio.run_command(
690 stdout, stderr = subprocessio.run_command(
689 cmd, env=os.environ.copy())
691 cmd, env=os.environ.copy())
690 extras = json.loads(base64.urlsafe_b64decode(stdout))
692 extras = json.loads(base64.urlsafe_b64decode(stdout))
691 except Exception:
693 except Exception:
692 log.exception('Failed to extract extras info from txn_id')
694 log.exception('Failed to extract extras info from txn_id')
693
695
694 return extras
696 return extras
695
697
696
698
697 def _get_extras_from_commit_id(commit_id, path):
699 def _get_extras_from_commit_id(commit_id, path):
698 extras = {}
700 extras = {}
699 try:
701 try:
700 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
702 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
701 '-r', commit_id,
703 '-r', commit_id,
702 '--revprop', path, 'rc-scm-extras']
704 '--revprop', path, 'rc-scm-extras']
703 stdout, stderr = subprocessio.run_command(
705 stdout, stderr = subprocessio.run_command(
704 cmd, env=os.environ.copy())
706 cmd, env=os.environ.copy())
705 extras = json.loads(base64.urlsafe_b64decode(stdout))
707 extras = json.loads(base64.urlsafe_b64decode(stdout))
706 except Exception:
708 except Exception:
707 log.exception('Failed to extract extras info from commit_id')
709 log.exception('Failed to extract extras info from commit_id')
708
710
709 return extras
711 return extras
710
712
711
713
712 def svn_pre_commit(repo_path, commit_data, env):
714 def svn_pre_commit(repo_path, commit_data, env):
713 path, txn_id = commit_data
715 path, txn_id = commit_data
714 branches = []
716 branches = []
715 tags = []
717 tags = []
716
718
717 if env.get('RC_SCM_DATA'):
719 if env.get('RC_SCM_DATA'):
718 extras = json.loads(env['RC_SCM_DATA'])
720 extras = json.loads(env['RC_SCM_DATA'])
719 else:
721 else:
720 # fallback method to read from TXN-ID stored data
722 # fallback method to read from TXN-ID stored data
721 extras = _get_extras_from_txn_id(path, txn_id)
723 extras = _get_extras_from_txn_id(path, txn_id)
722 if not extras:
724 if not extras:
723 return 0
725 return 0
724
726
725 extras['hook_type'] = 'pre_commit'
727 extras['hook_type'] = 'pre_commit'
726 extras['commit_ids'] = [txn_id]
728 extras['commit_ids'] = [txn_id]
727 extras['txn_id'] = txn_id
729 extras['txn_id'] = txn_id
728 extras['new_refs'] = {
730 extras['new_refs'] = {
729 'total_commits': 1,
731 'total_commits': 1,
730 'branches': branches,
732 'branches': branches,
731 'bookmarks': [],
733 'bookmarks': [],
732 'tags': tags,
734 'tags': tags,
733 }
735 }
734
736
735 return _call_hook('pre_push', extras, SvnMessageWriter())
737 return _call_hook('pre_push', extras, SvnMessageWriter())
736
738
737
739
738 def svn_post_commit(repo_path, commit_data, env):
740 def svn_post_commit(repo_path, commit_data, env):
739 """
741 """
740 commit_data is path, rev, txn_id
742 commit_data is path, rev, txn_id
741 """
743 """
742 if len(commit_data) == 3:
744 if len(commit_data) == 3:
743 path, commit_id, txn_id = commit_data
745 path, commit_id, txn_id = commit_data
744 elif len(commit_data) == 2:
746 elif len(commit_data) == 2:
745 log.error('Failed to extract txn_id from commit_data using legacy method. '
747 log.error('Failed to extract txn_id from commit_data using legacy method. '
746 'Some functionality might be limited')
748 'Some functionality might be limited')
747 path, commit_id = commit_data
749 path, commit_id = commit_data
748 txn_id = None
750 txn_id = None
749
751
750 branches = []
752 branches = []
751 tags = []
753 tags = []
752
754
753 if env.get('RC_SCM_DATA'):
755 if env.get('RC_SCM_DATA'):
754 extras = json.loads(env['RC_SCM_DATA'])
756 extras = json.loads(env['RC_SCM_DATA'])
755 else:
757 else:
756 # fallback method to read from TXN-ID stored data
758 # fallback method to read from TXN-ID stored data
757 extras = _get_extras_from_commit_id(commit_id, path)
759 extras = _get_extras_from_commit_id(commit_id, path)
758 if not extras:
760 if not extras:
759 return 0
761 return 0
760
762
761 extras['hook_type'] = 'post_commit'
763 extras['hook_type'] = 'post_commit'
762 extras['commit_ids'] = [commit_id]
764 extras['commit_ids'] = [commit_id]
763 extras['txn_id'] = txn_id
765 extras['txn_id'] = txn_id
764 extras['new_refs'] = {
766 extras['new_refs'] = {
765 'branches': branches,
767 'branches': branches,
766 'bookmarks': [],
768 'bookmarks': [],
767 'tags': tags,
769 'tags': tags,
768 'total_commits': 1,
770 'total_commits': 1,
769 }
771 }
770
772
771 if 'repo_size' in extras['hooks']:
773 if 'repo_size' in extras['hooks']:
772 try:
774 try:
773 _call_hook('repo_size', extras, SvnMessageWriter())
775 _call_hook('repo_size', extras, SvnMessageWriter())
774 except Exception:
776 except Exception:
775 pass
777 pass
776
778
777 return _call_hook('post_push', extras, SvnMessageWriter())
779 return _call_hook('post_push', extras, SvnMessageWriter())
@@ -1,414 +1,417 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 dulwich.protocol
24 import dulwich.protocol
25 from dulwich.protocol import CAPABILITY_SIDE_BAND, CAPABILITY_SIDE_BAND_64K
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.lib.rc_json import json
29 from vcsserver import hooks, subprocessio
29 from vcsserver import hooks, subprocessio
30 from vcsserver.str_utils import ascii_bytes
30 from vcsserver.str_utils import ascii_bytes
31
31
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class FileWrapper(object):
36 class FileWrapper(object):
37 """File wrapper that ensures how much data is read from it."""
37 """File wrapper that ensures how much data is read from it."""
38
38
39 def __init__(self, fd, content_length):
39 def __init__(self, fd, content_length):
40 self.fd = fd
40 self.fd = fd
41 self.content_length = content_length
41 self.content_length = content_length
42 self.remain = content_length
42 self.remain = content_length
43
43
44 def read(self, size):
44 def read(self, size):
45 if size <= self.remain:
45 if size <= self.remain:
46 try:
46 try:
47 data = self.fd.read(size)
47 data = self.fd.read(size)
48 except socket.error:
48 except socket.error:
49 raise IOError(self)
49 raise IOError(self)
50 self.remain -= size
50 self.remain -= size
51 elif self.remain:
51 elif self.remain:
52 data = self.fd.read(self.remain)
52 data = self.fd.read(self.remain)
53 self.remain = 0
53 self.remain = 0
54 else:
54 else:
55 data = None
55 data = None
56 return data
56 return data
57
57
58 def __repr__(self):
58 def __repr__(self):
59 return '<FileWrapper {} len: {}, read: {}>'.format(
59 return '<FileWrapper {} len: {}, read: {}>'.format(
60 self.fd, self.content_length, self.content_length - self.remain
60 self.fd, self.content_length, self.content_length - self.remain
61 )
61 )
62
62
63
63
64 class GitRepository(object):
64 class GitRepository(object):
65 """WSGI app for handling Git smart protocol endpoints."""
65 """WSGI app for handling Git smart protocol endpoints."""
66
66
67 git_folder_signature = frozenset(('config', 'head', 'info', 'objects', 'refs'))
67 git_folder_signature = frozenset(('config', 'head', 'info', 'objects', 'refs'))
68 commands = frozenset(('git-upload-pack', 'git-receive-pack'))
68 commands = frozenset(('git-upload-pack', 'git-receive-pack'))
69 valid_accepts = frozenset(f'application/x-{c}-result' for c in commands)
69 valid_accepts = frozenset(f'application/x-{c}-result' 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 b'PACK\x00\x00\x00\x02\x00\x00\x00\x00\x02\x9d\x08' +
73 b'PACK\x00\x00\x00\x02\x00\x00\x00\x00\x02\x9d\x08' +
74 b'\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 FLUSH_PACKET = b"0000"
76 FLUSH_PACKET = b"0000"
77
77
78 SIDE_BAND_CAPS = frozenset((CAPABILITY_SIDE_BAND, CAPABILITY_SIDE_BAND_64K))
78 SIDE_BAND_CAPS = frozenset((CAPABILITY_SIDE_BAND, CAPABILITY_SIDE_BAND_64K))
79
79
80 def __init__(self, repo_name, content_path, git_path, update_server_info, extras):
80 def __init__(self, repo_name, content_path, git_path, update_server_info, extras):
81 files = frozenset(f.lower() for f in os.listdir(content_path))
81 files = frozenset(f.lower() for f in os.listdir(content_path))
82 valid_dir_signature = self.git_folder_signature.issubset(files)
82 valid_dir_signature = self.git_folder_signature.issubset(files)
83
83
84 if not valid_dir_signature:
84 if not valid_dir_signature:
85 raise OSError('%s missing git signature' % content_path)
85 raise OSError(f'{content_path} missing git signature')
86
86
87 self.content_path = content_path
87 self.content_path = content_path
88 self.repo_name = repo_name
88 self.repo_name = repo_name
89 self.extras = extras
89 self.extras = extras
90 self.git_path = git_path
90 self.git_path = git_path
91 self.update_server_info = update_server_info
91 self.update_server_info = update_server_info
92
92
93 def _get_fixedpath(self, path):
93 def _get_fixedpath(self, path):
94 """
94 """
95 Small fix for repo_path
95 Small fix for repo_path
96
96
97 :param path:
97 :param path:
98 """
98 """
99 path = path.split(self.repo_name, 1)[-1]
99 path = path.split(self.repo_name, 1)[-1]
100 if path.startswith('.git'):
100 if path.startswith('.git'):
101 # 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
102 # here, and remove from the service command
102 # here, and remove from the service command
103 path = path[4:]
103 path = path[4:]
104
104
105 return path.strip('/')
105 return path.strip('/')
106
106
107 def inforefs(self, request, unused_environ):
107 def inforefs(self, request, unused_environ):
108 """
108 """
109 WSGI Response producer for HTTP GET Git Smart
109 WSGI Response producer for HTTP GET Git Smart
110 HTTP /info/refs request.
110 HTTP /info/refs request.
111 """
111 """
112
112
113 git_command = request.GET.get('service')
113 git_command = request.GET.get('service')
114 if git_command not in self.commands:
114 if git_command not in self.commands:
115 log.debug('command %s not allowed', git_command)
115 log.debug('command %s not allowed', git_command)
116 return exc.HTTPForbidden()
116 return exc.HTTPForbidden()
117
117
118 # 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
119 # line count by 1.
119 # line count by 1.
120 # by git docs: Documentation/technical/http-protocol.txt#L214 \n is
120 # by git docs: Documentation/technical/http-protocol.txt#L214 \n is
121 # a part of protocol.
121 # a part of protocol.
122 # 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
123 # blows up if you sprinkle "flush" (0000) as "0001\n".
123 # blows up if you sprinkle "flush" (0000) as "0001\n".
124 # It reads binary, per number of bytes specified.
124 # It reads binary, per number of bytes specified.
125 # if you do add '\n' as part of data, count it.
125 # if you do add '\n' as part of data, count it.
126 server_advert = '# service=%s\n' % git_command
126 server_advert = '# service=%s\n' % git_command
127 packet_len = hex(len(server_advert) + 4)[2:].rjust(4, '0').lower()
127 packet_len = hex(len(server_advert) + 4)[2:].rjust(4, '0').lower()
128 try:
128 try:
129 gitenv = dict(os.environ)
129 gitenv = dict(os.environ)
130 # forget all configs
130 # forget all configs
131 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
131 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
132 command = [self.git_path, git_command[4:], '--stateless-rpc',
132 command = [self.git_path, git_command[4:], '--stateless-rpc',
133 '--advertise-refs', self.content_path]
133 '--advertise-refs', self.content_path]
134 out = subprocessio.SubprocessIOChunker(
134 out = subprocessio.SubprocessIOChunker(
135 command,
135 command,
136 env=gitenv,
136 env=gitenv,
137 starting_values=[ascii_bytes(packet_len + server_advert) + self.FLUSH_PACKET],
137 starting_values=[ascii_bytes(packet_len + server_advert) + self.FLUSH_PACKET],
138 shell=False
138 shell=False
139 )
139 )
140 except OSError:
140 except OSError:
141 log.exception('Error processing command')
141 log.exception('Error processing command')
142 raise exc.HTTPExpectationFailed()
142 raise exc.HTTPExpectationFailed()
143
143
144 resp = Response()
144 resp = Response()
145 resp.content_type = f'application/x-{git_command}-advertisement'
145 resp.content_type = f'application/x-{git_command}-advertisement'
146 resp.charset = None
146 resp.charset = None
147 resp.app_iter = out
147 resp.app_iter = out
148
148
149 return resp
149 return resp
150
150
151 def _get_want_capabilities(self, request):
151 def _get_want_capabilities(self, request):
152 """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."""
153 pos = request.body_file_seekable.tell()
153 pos = request.body_file_seekable.tell()
154 first_line = request.body_file_seekable.readline()
154 first_line = request.body_file_seekable.readline()
155 request.body_file_seekable.seek(pos)
155 request.body_file_seekable.seek(pos)
156
156
157 return frozenset(
157 return frozenset(
158 dulwich.protocol.extract_want_line_capabilities(first_line)[1])
158 dulwich.protocol.extract_want_line_capabilities(first_line)[1])
159
159
160 def _build_failed_pre_pull_response(self, capabilities, pre_pull_messages):
160 def _build_failed_pre_pull_response(self, capabilities, pre_pull_messages):
161 """
161 """
162 Construct a response with an empty PACK file.
162 Construct a response with an empty PACK file.
163
163
164 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
165 or clone command.
165 or clone command.
166
166
167 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
168 was aborted.
168 was aborted.
169
169
170 If additionally, the user is accepting messages we send them the output
170 If additionally, the user is accepting messages we send them the output
171 of the pre-pull hook.
171 of the pre-pull hook.
172
172
173 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
174 emtpy PACK file.
174 emtpy PACK file.
175 """
175 """
176
176
177 if self.SIDE_BAND_CAPS.intersection(capabilities):
177 if self.SIDE_BAND_CAPS.intersection(capabilities):
178 response = []
178 response = []
179 proto = dulwich.protocol.Protocol(None, response.append)
179 proto = dulwich.protocol.Protocol(None, response.append)
180 proto.write_pkt_line(dulwich.protocol.NAK_LINE)
180 proto.write_pkt_line(dulwich.protocol.NAK_LINE)
181
181
182 self._write_sideband_to_proto(proto, ascii_bytes(pre_pull_messages, allow_bytes=True), capabilities)
182 self._write_sideband_to_proto(proto, ascii_bytes(pre_pull_messages, allow_bytes=True), capabilities)
183 # 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
184 # produces a fatal error in the client:
184 # produces a fatal error in the client:
185 # fatal: error in sideband demultiplexer
185 # fatal: error in sideband demultiplexer
186 proto.write_sideband(
186 proto.write_sideband(
187 dulwich.protocol.SIDE_BAND_CHANNEL_PROGRESS,
187 dulwich.protocol.SIDE_BAND_CHANNEL_PROGRESS,
188 ascii_bytes('Pre pull hook failed: aborting\n', allow_bytes=True))
188 ascii_bytes('Pre pull hook failed: aborting\n', allow_bytes=True))
189 proto.write_sideband(
189 proto.write_sideband(
190 dulwich.protocol.SIDE_BAND_CHANNEL_DATA,
190 dulwich.protocol.SIDE_BAND_CHANNEL_DATA,
191 ascii_bytes(self.EMPTY_PACK, allow_bytes=True))
191 ascii_bytes(self.EMPTY_PACK, allow_bytes=True))
192
192
193 # writes b"0000" as default
193 # writes b"0000" as default
194 proto.write_pkt_line(None)
194 proto.write_pkt_line(None)
195
195
196 return response
196 return response
197 else:
197 else:
198 return [ascii_bytes(self.EMPTY_PACK, allow_bytes=True)]
198 return [ascii_bytes(self.EMPTY_PACK, allow_bytes=True)]
199
199
200 def _build_post_pull_response(self, response, capabilities, start_message, end_message):
200 def _build_post_pull_response(self, response, capabilities, start_message, end_message):
201 """
201 """
202 Given a list response we inject the post-pull messages.
202 Given a list response we inject the post-pull messages.
203
203
204 We only inject the messages if the client supports sideband, and the
204 We only inject the messages if the client supports sideband, and the
205 response has the format:
205 response has the format:
206 0008NAK\n...0000
206 0008NAK\n...0000
207
207
208 Note that we do not check the no-progress capability as by default, git
208 Note that we do not check the no-progress capability as by default, git
209 sends it, which effectively would block all messages.
209 sends it, which effectively would block all messages.
210 """
210 """
211
211
212 if not self.SIDE_BAND_CAPS.intersection(capabilities):
212 if not self.SIDE_BAND_CAPS.intersection(capabilities):
213 return response
213 return response
214
214
215 if not start_message and not end_message:
215 if not start_message and not end_message:
216 return response
216 return response
217
217
218 try:
218 try:
219 iter(response)
219 iter(response)
220 # iterator probably will work, we continue
220 # iterator probably will work, we continue
221 except TypeError:
221 except TypeError:
222 raise TypeError(f'response must be an iterator: got {type(response)}')
222 raise TypeError(f'response must be an iterator: got {type(response)}')
223 if isinstance(response, (list, tuple)):
223 if isinstance(response, (list, tuple)):
224 raise TypeError(f'response must be an iterator: got {type(response)}')
224 raise TypeError(f'response must be an iterator: got {type(response)}')
225
225
226 def injected_response():
226 def injected_response():
227
227
228 do_loop = 1
228 do_loop = 1
229 header_injected = 0
229 header_injected = 0
230 next_item = None
230 next_item = None
231 has_item = False
231 has_item = False
232 item = b''
233
232 while do_loop:
234 while do_loop:
233
235
234 try:
236 try:
235 next_item = next(response)
237 next_item = next(response)
236 except StopIteration:
238 except StopIteration:
237 do_loop = 0
239 do_loop = 0
238
240
239 if has_item:
241 if has_item:
240 # last item ! alter it now
242 # last item ! alter it now
241 if do_loop == 0 and item.endswith(self.FLUSH_PACKET):
243 if do_loop == 0 and item.endswith(self.FLUSH_PACKET):
242 new_response = [item[:-4]]
244 new_response = [item[:-4]]
243 new_response.extend(self._get_messages(end_message, capabilities))
245 new_response.extend(self._get_messages(end_message, capabilities))
244 new_response.append(self.FLUSH_PACKET)
246 new_response.append(self.FLUSH_PACKET)
245 item = b''.join(new_response)
247 item = b''.join(new_response)
246
248
247 yield item
249 yield item
250
248 has_item = True
251 has_item = True
249 item = next_item
252 item = next_item
250
253
251 # alter item if it's the initial chunk
254 # alter item if it's the initial chunk
252 if not header_injected and item.startswith(b'0008NAK\n'):
255 if not header_injected and item.startswith(b'0008NAK\n'):
253 new_response = [b'0008NAK\n']
256 new_response = [b'0008NAK\n']
254 new_response.extend(self._get_messages(start_message, capabilities))
257 new_response.extend(self._get_messages(start_message, capabilities))
255 new_response.append(item[8:])
258 new_response.append(item[8:])
256 item = b''.join(new_response)
259 item = b''.join(new_response)
257 header_injected = 1
260 header_injected = 1
258
261
259 return injected_response()
262 return injected_response()
260
263
261 def _write_sideband_to_proto(self, proto, data, capabilities):
264 def _write_sideband_to_proto(self, proto, data, capabilities):
262 """
265 """
263 Write the data to the proto's sideband number 2 == SIDE_BAND_CHANNEL_PROGRESS
266 Write the data to the proto's sideband number 2 == SIDE_BAND_CHANNEL_PROGRESS
264
267
265 We do not use dulwich's write_sideband directly as it only supports
268 We do not use dulwich's write_sideband directly as it only supports
266 side-band-64k.
269 side-band-64k.
267 """
270 """
268 if not data:
271 if not data:
269 return
272 return
270
273
271 # N.B.(skreft): The values below are explained in the pack protocol
274 # N.B.(skreft): The values below are explained in the pack protocol
272 # documentation, section Packfile Data.
275 # documentation, section Packfile Data.
273 # https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt
276 # https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt
274 if CAPABILITY_SIDE_BAND_64K in capabilities:
277 if CAPABILITY_SIDE_BAND_64K in capabilities:
275 chunk_size = 65515
278 chunk_size = 65515
276 elif CAPABILITY_SIDE_BAND in capabilities:
279 elif CAPABILITY_SIDE_BAND in capabilities:
277 chunk_size = 995
280 chunk_size = 995
278 else:
281 else:
279 return
282 return
280
283
281 chunker = (data[i:i + chunk_size] for i in range(0, len(data), chunk_size))
284 chunker = (data[i:i + chunk_size] for i in range(0, len(data), chunk_size))
282
285
283 for chunk in chunker:
286 for chunk in chunker:
284 proto.write_sideband(dulwich.protocol.SIDE_BAND_CHANNEL_PROGRESS, ascii_bytes(chunk, allow_bytes=True))
287 proto.write_sideband(dulwich.protocol.SIDE_BAND_CHANNEL_PROGRESS, ascii_bytes(chunk, allow_bytes=True))
285
288
286 def _get_messages(self, data, capabilities):
289 def _get_messages(self, data, capabilities):
287 """Return a list with packets for sending data in sideband number 2."""
290 """Return a list with packets for sending data in sideband number 2."""
288 response = []
291 response = []
289 proto = dulwich.protocol.Protocol(None, response.append)
292 proto = dulwich.protocol.Protocol(None, response.append)
290
293
291 self._write_sideband_to_proto(proto, data, capabilities)
294 self._write_sideband_to_proto(proto, data, capabilities)
292
295
293 return response
296 return response
294
297
295 def backend(self, request, environ):
298 def backend(self, request, environ):
296 """
299 """
297 WSGI Response producer for HTTP POST Git Smart HTTP requests.
300 WSGI Response producer for HTTP POST Git Smart HTTP requests.
298 Reads commands and data from HTTP POST's body.
301 Reads commands and data from HTTP POST's body.
299 returns an iterator obj with contents of git command's
302 returns an iterator obj with contents of git command's
300 response to stdout
303 response to stdout
301 """
304 """
302 # TODO(skreft): think how we could detect an HTTPLockedException, as
305 # TODO(skreft): think how we could detect an HTTPLockedException, as
303 # we probably want to have the same mechanism used by mercurial and
306 # we probably want to have the same mechanism used by mercurial and
304 # simplevcs.
307 # simplevcs.
305 # For that we would need to parse the output of the command looking for
308 # For that we would need to parse the output of the command looking for
306 # some signs of the HTTPLockedError, parse the data and reraise it in
309 # some signs of the HTTPLockedError, parse the data and reraise it in
307 # pygrack. However, that would interfere with the streaming.
310 # pygrack. However, that would interfere with the streaming.
308 #
311 #
309 # Now the output of a blocked push is:
312 # Now the output of a blocked push is:
310 # Pushing to http://test_regular:test12@127.0.0.1:5001/vcs_test_git
313 # Pushing to http://test_regular:test12@127.0.0.1:5001/vcs_test_git
311 # POST git-receive-pack (1047 bytes)
314 # POST git-receive-pack (1047 bytes)
312 # remote: ERROR: Repository `vcs_test_git` locked by user `test_admin`. Reason:`lock_auto`
315 # remote: ERROR: Repository `vcs_test_git` locked by user `test_admin`. Reason:`lock_auto`
313 # To http://test_regular:test12@127.0.0.1:5001/vcs_test_git
316 # To http://test_regular:test12@127.0.0.1:5001/vcs_test_git
314 # ! [remote rejected] master -> master (pre-receive hook declined)
317 # ! [remote rejected] master -> master (pre-receive hook declined)
315 # error: failed to push some refs to 'http://test_regular:test12@127.0.0.1:5001/vcs_test_git'
318 # error: failed to push some refs to 'http://test_regular:test12@127.0.0.1:5001/vcs_test_git'
316
319
317 git_command = self._get_fixedpath(request.path_info)
320 git_command = self._get_fixedpath(request.path_info)
318 if git_command not in self.commands:
321 if git_command not in self.commands:
319 log.debug('command %s not allowed', git_command)
322 log.debug('command %s not allowed', git_command)
320 return exc.HTTPForbidden()
323 return exc.HTTPForbidden()
321
324
322 capabilities = None
325 capabilities = None
323 if git_command == 'git-upload-pack':
326 if git_command == 'git-upload-pack':
324 capabilities = self._get_want_capabilities(request)
327 capabilities = self._get_want_capabilities(request)
325
328
326 if 'CONTENT_LENGTH' in environ:
329 if 'CONTENT_LENGTH' in environ:
327 inputstream = FileWrapper(request.body_file_seekable,
330 inputstream = FileWrapper(request.body_file_seekable,
328 request.content_length)
331 request.content_length)
329 else:
332 else:
330 inputstream = request.body_file_seekable
333 inputstream = request.body_file_seekable
331
334
332 resp = Response()
335 resp = Response()
333 resp.content_type = f'application/x-{git_command}-result'
336 resp.content_type = f'application/x-{git_command}-result'
334 resp.charset = None
337 resp.charset = None
335
338
336 pre_pull_messages = ''
339 pre_pull_messages = ''
337 # Upload-pack == clone
340 # Upload-pack == clone
338 if git_command == 'git-upload-pack':
341 if git_command == 'git-upload-pack':
339 hook_response = hooks.git_pre_pull(self.extras)
342 hook_response = hooks.git_pre_pull(self.extras)
340 if hook_response.status != 0:
343 if hook_response.status != 0:
341 pre_pull_messages = hook_response.output
344 pre_pull_messages = hook_response.output
342 resp.app_iter = self._build_failed_pre_pull_response(
345 resp.app_iter = self._build_failed_pre_pull_response(
343 capabilities, pre_pull_messages)
346 capabilities, pre_pull_messages)
344 return resp
347 return resp
345
348
346 gitenv = dict(os.environ)
349 gitenv = dict(os.environ)
347 # forget all configs
350 # forget all configs
348 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
351 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
349 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
352 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
350 cmd = [self.git_path, git_command[4:], '--stateless-rpc',
353 cmd = [self.git_path, git_command[4:], '--stateless-rpc',
351 self.content_path]
354 self.content_path]
352 log.debug('handling cmd %s', cmd)
355 log.debug('handling cmd %s', cmd)
353
356
354 out = subprocessio.SubprocessIOChunker(
357 out = subprocessio.SubprocessIOChunker(
355 cmd,
358 cmd,
356 input_stream=inputstream,
359 input_stream=inputstream,
357 env=gitenv,
360 env=gitenv,
358 cwd=self.content_path,
361 cwd=self.content_path,
359 shell=False,
362 shell=False,
360 fail_on_stderr=False,
363 fail_on_stderr=False,
361 fail_on_return_code=False
364 fail_on_return_code=False
362 )
365 )
363
366
364 if self.update_server_info and git_command == 'git-receive-pack':
367 if self.update_server_info and git_command == 'git-receive-pack':
365 # We need to fully consume the iterator here, as the
368 # We need to fully consume the iterator here, as the
366 # update-server-info command needs to be run after the push.
369 # update-server-info command needs to be run after the push.
367 out = list(out)
370 out = list(out)
368
371
369 # Updating refs manually after each push.
372 # Updating refs manually after each push.
370 # This is required as some clients are exposing Git repos internally
373 # This is required as some clients are exposing Git repos internally
371 # with the dumb protocol.
374 # with the dumb protocol.
372 cmd = [self.git_path, 'update-server-info']
375 cmd = [self.git_path, 'update-server-info']
373 log.debug('handling cmd %s', cmd)
376 log.debug('handling cmd %s', cmd)
374 output = subprocessio.SubprocessIOChunker(
377 output = subprocessio.SubprocessIOChunker(
375 cmd,
378 cmd,
376 input_stream=inputstream,
379 input_stream=inputstream,
377 env=gitenv,
380 env=gitenv,
378 cwd=self.content_path,
381 cwd=self.content_path,
379 shell=False,
382 shell=False,
380 fail_on_stderr=False,
383 fail_on_stderr=False,
381 fail_on_return_code=False
384 fail_on_return_code=False
382 )
385 )
383 # Consume all the output so the subprocess finishes
386 # Consume all the output so the subprocess finishes
384 for _ in output:
387 for _ in output:
385 pass
388 pass
386
389
387 # Upload-pack == clone
390 # Upload-pack == clone
388 if git_command == 'git-upload-pack':
391 if git_command == 'git-upload-pack':
389 hook_response = hooks.git_post_pull(self.extras)
392 hook_response = hooks.git_post_pull(self.extras)
390 post_pull_messages = hook_response.output
393 post_pull_messages = hook_response.output
391 resp.app_iter = self._build_post_pull_response(out, capabilities, pre_pull_messages, post_pull_messages)
394 resp.app_iter = self._build_post_pull_response(out, capabilities, pre_pull_messages, post_pull_messages)
392 else:
395 else:
393 resp.app_iter = out
396 resp.app_iter = out
394
397
395 return resp
398 return resp
396
399
397 def __call__(self, environ, start_response):
400 def __call__(self, environ, start_response):
398 request = Request(environ)
401 request = Request(environ)
399 _path = self._get_fixedpath(request.path_info)
402 _path = self._get_fixedpath(request.path_info)
400 if _path.startswith('info/refs'):
403 if _path.startswith('info/refs'):
401 app = self.inforefs
404 app = self.inforefs
402 else:
405 else:
403 app = self.backend
406 app = self.backend
404
407
405 try:
408 try:
406 resp = app(request, environ)
409 resp = app(request, environ)
407 except exc.HTTPException as error:
410 except exc.HTTPException as error:
408 log.exception('HTTP Error')
411 log.exception('HTTP Error')
409 resp = error
412 resp = error
410 except Exception:
413 except Exception:
411 log.exception('Unknown error')
414 log.exception('Unknown error')
412 resp = exc.HTTPInternalServerError()
415 resp = exc.HTTPInternalServerError()
413
416
414 return resp(environ, start_response)
417 return resp(environ, start_response)
@@ -1,19 +1,17 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
1 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,62 +1,67 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 logging
19 import logging
20
20
21 log = logging.getLogger(__name__)
21 log = logging.getLogger(__name__)
22
22
23
23
24 def str2bool(str_):
24 def str2bool(str_):
25 """
25 """
26 returns True/False value from given string, it tries to translate the
26 returns True/False value from given string, it tries to translate the
27 string into boolean
27 string into boolean
28
28
29 :param str_: string value to translate into boolean
29 :param str_: string value to translate into boolean
30 :rtype: boolean
30 :rtype: boolean
31 :returns: boolean from given string
31 :returns: boolean from given string
32 """
32 """
33 if str_ is None:
33 if str_ is None:
34 return False
34 return False
35 if str_ in (True, False):
35 if str_ in (True, False):
36 return str_
36 return str_
37 str_ = str(str_).strip().lower()
37 str_ = str(str_).strip().lower()
38 return str_ in ('t', 'true', 'y', 'yes', 'on', '1')
38 return str_ in ('t', 'true', 'y', 'yes', 'on', '1')
39
39
40
40
41 def aslist(obj, sep=None, strip=True) -> list:
41 def aslist(obj, sep=None, strip=True) -> list:
42 """
42 """
43 Returns given string separated by sep as list
43 Returns given string separated by sep as list
44
44
45 :param obj:
45 :param obj:
46 :param sep:
46 :param sep:
47 :param strip:
47 :param strip:
48 """
48 """
49 if isinstance(obj, str):
49 if isinstance(obj, str):
50 if obj in ['', ""]:
50 if obj in ['', ""]:
51 return []
51 return []
52
52
53 lst = obj.split(sep)
53 lst = obj.split(sep)
54 if strip:
54 if strip:
55 lst = [v.strip() for v in lst]
55 lst = [v.strip() for v in lst]
56 return lst
56 return lst
57 elif isinstance(obj, (list, tuple)):
57 elif isinstance(obj, (list, tuple)):
58 return obj
58 return obj
59 elif obj is None:
59 elif obj is None:
60 return []
60 return []
61 else:
61 else:
62 return [obj] No newline at end of file
62 return [obj]
63
64
65 def assert_bytes(input_type, expected_types=(bytes,)):
66 if not isinstance(input_type, expected_types):
67 raise ValueError(f'input_types should be one of {expected_types} got {type(input_type)} instead')
@@ -1,47 +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
21 class RemoteBase(object):
21 class RemoteBase(object):
22 EMPTY_COMMIT = '0' * 40
22 EMPTY_COMMIT = '0' * 40
23
23
24 def _region(self, wire):
24 def _region(self, wire):
25 cache_repo_id = wire.get('cache_repo_id', '')
25 cache_repo_id = wire.get('cache_repo_id', '')
26 cache_namespace_uid = f'cache_repo.{cache_repo_id}'
26 cache_namespace_uid = f'cache_repo.{cache_repo_id}'
27 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)
28
28
29 def _cache_on(self, wire):
29 def _cache_on(self, wire):
30 context = wire.get('context', '')
30 context = wire.get('context', '')
31 context_uid = f'{context}'
31 context_uid = f'{context}'
32 repo_id = wire.get('repo_id', '')
32 repo_id = wire.get('repo_id', '')
33 cache = wire.get('cache', True)
33 cache = wire.get('cache', True)
34 cache_on = context and cache
34 cache_on = context and cache
35 return cache_on, context_uid, repo_id
35 return cache_on, context_uid, repo_id
36
36
37 def vcsserver_invalidate_cache(self, wire, delete):
37 def vcsserver_invalidate_cache(self, wire, delete):
38 from vcsserver.lib import rc_cache
38 from vcsserver.lib import rc_cache
39 repo_id = wire.get('repo_id', '')
39 repo_id = wire.get('repo_id', '')
40 cache_repo_id = wire.get('cache_repo_id', '')
40 cache_repo_id = wire.get('cache_repo_id', '')
41 cache_namespace_uid = f'cache_repo.{cache_repo_id}'
41 cache_namespace_uid = f'cache_repo.{cache_repo_id}'
42
42
43 if delete:
43 if delete:
44 rc_cache.clear_cache_namespace(
44 rc_cache.clear_cache_namespace(
45 'repo_object', cache_namespace_uid, invalidate=True)
45 'repo_object', cache_namespace_uid, method=rc_cache.CLEAR_DELETE)
46
46
47 return {'invalidated': {'repo_id': repo_id, 'delete': delete}}
47 return {'invalidated': {'repo_id': repo_id, 'delete': delete}}
General Comments 0
You need to be logged in to leave comments. Login now